fix: user init mail (for wrong email) (#891)

* add resendInitialMail

* disable email notifications (when not initialised)

* fix resend init mail

* add tests

* cleanup

* cleanup

* fix tests

* add resend trigger, dialog

* refactor contact component, add sendinitmail fnc

* skip email if empty

* reload user on phone email changes, i18n warndialog on dl

* lint

* rebuild mgmt proto

* remove initial focus

* Update console/src/assets/i18n/de.json

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>

Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
This commit is contained in:
Livio Amstutz
2020-11-16 11:43:22 +01:00
committed by GitHub
parent 69c39b5eb2
commit 376fba72d8
42 changed files with 11465 additions and 18601 deletions

View File

@@ -4,6 +4,7 @@
padding: 1.5rem;
border-radius: .5rem;
padding-top: 1rem;
min-width: 350px;
.header {
margin-top: 0;

View File

@@ -11,6 +11,10 @@
.action {
display: flex;
button {
border-radius: .5rem;
}
.ok-button {
margin-left: .5rem;
}

View File

@@ -40,10 +40,15 @@
<app-card *ngIf="user" title="{{ 'USER.LOGINMETHODS.TITLE' | translate }}"
description="{{ 'USER.LOGINMETHODS.DESCRIPTION' | translate }}">
<app-contact *ngIf="user?.human" [human]="user.human" (savedPhone)="savePhone($event)"
(savedEmail)="saveEmail($event)" (enteredPhoneCode)="enteredPhoneCode($event)"
(deletedPhone)="deletePhone()" (resendEmailVerification)="resendEmailVerification()"
(resendPhoneVerification)="resendPhoneVerification()"></app-contact>
<button card-actions mat-icon-button (click)="refreshUser()">
<mat-icon>refresh</mat-icon>
</button>
<app-contact *ngIf="user?.human" [human]="user.human" [state]="user.state" canWrite="true"
[userStateEnum]="UserState" (editType)="openEditDialog($event)"
(enteredPhoneCode)="enteredPhoneCode($event)" (deletedPhone)="deletePhone()"
(resendEmailVerification)="resendEmailVerification()"
(resendPhoneVerification)="resendPhoneVerification()">
</app-contact>
</app-card>
<app-auth-user-mfa *ngIf="user" #mfaComponent></app-auth-user-mfa>

View File

@@ -98,3 +98,7 @@
}
}
}
.resendemail {
margin-right: 1rem;
}

View File

@@ -1,12 +1,24 @@
import { Component, OnDestroy } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs';
import { ChangeType } from 'src/app/modules/changes/changes.component';
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
import { Gender, UserAddress, UserEmail, UserPhone, UserProfile, UserView } from 'src/app/proto/generated/auth_pb';
import {
Gender,
UserAddress,
UserEmail,
UserPhone,
UserProfile,
UserState,
UserView,
} from 'src/app/proto/generated/auth_pb';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service';
import { EditDialogType } from '../user-detail/user-detail.component';
import { EditDialogComponent } from './edit-dialog/edit-dialog.component';
@Component({
selector: 'app-auth-user-detail',
templateUrl: './auth-user-detail.component.html',
@@ -26,6 +38,7 @@ export class AuthUserDetailComponent implements OnDestroy {
public ChangeType: any = ChangeType;
public userLoginMustBeDomain: boolean = false;
public UserState: any = UserState;
public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.USER;
@@ -33,8 +46,13 @@ export class AuthUserDetailComponent implements OnDestroy {
public translate: TranslateService,
private toast: ToastService,
public userService: GrpcAuthService,
private dialog: MatDialog,
) {
this.loading = true;
this.refreshUser();
}
refreshUser(): void {
this.userService.GetMyUser().then(user => {
this.user = user.toObject();
this.loading = false;
@@ -81,6 +99,7 @@ export class AuthUserDetailComponent implements OnDestroy {
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
if (this.user.human) {
this.user.human.email = data.toObject().email;
this.refreshUser();
}
}).catch(error => {
this.toast.showError(error);
@@ -90,6 +109,7 @@ export class AuthUserDetailComponent implements OnDestroy {
public enteredPhoneCode(code: string): void {
this.userService.VerifyMyUserPhone(code).then(() => {
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
this.refreshUser();
}).catch(error => {
this.toast.showError(error);
});
@@ -99,14 +119,6 @@ export class AuthUserDetailComponent implements OnDestroy {
this.translate.use(language);
}
public resendEmailVerification(): void {
this.userService.ResendEmailVerification().then(() => {
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
}).catch(error => {
this.toast.showError(error);
});
}
public resendPhoneVerification(): void {
this.userService.ResendPhoneVerification().then(() => {
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true);
@@ -115,11 +127,20 @@ export class AuthUserDetailComponent implements OnDestroy {
});
}
public resendEmailVerification(): void {
this.userService.ResendMyEmailVerificationMail().then(() => {
this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true);
}).catch(error => {
this.toast.showError(error);
});
}
public deletePhone(): void {
this.userService.RemoveMyUserPhone().then(() => {
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
if (this.user.human) {
this.user.human.phone = '';
this.refreshUser();
}
}).catch(error => {
this.toast.showError(error);
@@ -133,10 +154,54 @@ export class AuthUserDetailComponent implements OnDestroy {
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
if (this.user.human) {
this.user.human.phone = data.toObject().phone;
this.refreshUser();
}
}).catch(error => {
this.toast.showError(error);
});
}
}
public openEditDialog(type: EditDialogType): void {
switch (type) {
case EditDialogType.PHONE:
const dialogRefPhone = this.dialog.open(EditDialogComponent, {
data: {
confirmKey: 'ACTIONS.SAVE',
cancelKey: 'ACTIONS.CANCEL',
labelKey: 'ACTIONS.NEWVALUE',
titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC',
value: this.user.human?.phone,
},
width: '400px',
});
dialogRefPhone.afterClosed().subscribe(resp => {
if (resp) {
this.savePhone(resp);
}
});
break;
case EditDialogType.EMAIL:
const dialogRefEmail = this.dialog.open(EditDialogComponent, {
data: {
confirmKey: 'ACTIONS.SAVE',
cancelKey: 'ACTIONS.CANCEL',
labelKey: 'ACTIONS.NEWVALUE',
titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC',
value: this.user.human?.email,
},
width: '400px',
});
dialogRefEmail.afterClosed().subscribe(resp => {
if (resp) {
this.saveEmail(resp);
}
});
break;
}
}
}

View File

@@ -0,0 +1,20 @@
<h1 mat-dialog-title>
<span class="title">{{data.titleKey | translate}}</span>
</h1>
<p class="desc">{{data.descriptionKey | translate}}</p>
<div mat-dialog-content>
<mat-form-field class="formfield">
<mat-label>{{data.labelKey | translate }}</mat-label>
<input matInput [(ngModel)]="value" />
</mat-form-field>
</div>
<div mat-dialog-actions class="action">
<button cdkFocusInitial color="primary" mat-button class="ok-button" (click)="closeDialog()">
{{data.cancelKey | translate}}
</button>
<button [disabled]="!value" cdkFocusInitial color="primary" mat-raised-button class="ok-button"
(click)="closeDialogWithValue(value)">
{{data.confirmKey | translate}}
</button>
</div>

View File

@@ -0,0 +1,21 @@
.formfield {
width: 100%;
}
.desc {
font-size: 14px;
color: var(--grey);
}
.action {
display: flex;
justify-content: flex-end;
.ok-button {
margin-left: .5rem;
}
button {
border-radius: .5rem;
}
}

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CodeDialogComponent } from './code-dialog.component';
describe('CodeDialogComponent', () => {
let component: CodeDialogComponent;
let fixture: ComponentFixture<CodeDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [CodeDialogComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CodeDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,23 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'app-edit-email-dialog',
templateUrl: './edit-dialog.component.html',
styleUrls: ['./edit-dialog.component.scss'],
})
export class EditDialogComponent {
public value: string = '';
constructor(public dialogRef: MatDialogRef<EditDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) {
this.value = data.value;
}
closeDialog(email: string = ''): void {
this.dialogRef.close(email);
}
closeDialogWithValue(value: string = ''): void {
this.dialogRef.close(value);
}
}

View File

@@ -0,0 +1,19 @@
<h1 mat-dialog-title>
<span class="title">{{'USER.SENDEMAILDIALOG.TITLE' | translate}} {{data?.number}}</span>
</h1>
<p class="desc">{{'USER.SENDEMAILDIALOG.DESCRIPTION' | translate}}</p>
<div mat-dialog-content>
<mat-form-field class="formfield">
<mat-label>{{ 'USER.SENDEMAILDIALOG.NEWEMAIL' | translate }}</mat-label>
<input matInput [(ngModel)]="email" />
</mat-form-field>
</div>
<div mat-dialog-actions class="action">
<button color="primary" mat-button class="ok-button" (click)="closeDialog()">
{{'ACTIONS.CLOSE' | translate}}
</button>
<button cdkFocusInitial color="primary" mat-raised-button class="ok-button" (click)="closeDialogWithSend(email)">
{{'ACTIONS.SEND' | translate}}
</button>
</div>

View File

@@ -0,0 +1,21 @@
.formfield {
width: 100%;
}
.desc {
font-size: 14px;
color: var(--grey);
}
.action {
display: flex;
justify-content: flex-end;
.ok-button {
margin-left: .5rem;
}
button {
border-radius: .5rem;
}
}

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CodeDialogComponent } from './code-dialog.component';
describe('CodeDialogComponent', () => {
let component: CodeDialogComponent;
let fixture: ComponentFixture<CodeDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [CodeDialogComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CodeDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,21 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'app-resend-email-dialog',
templateUrl: './resend-email-dialog.component.html',
styleUrls: ['./resend-email-dialog.component.scss'],
})
export class ResendEmailDialogComponent {
public email: string = '';
constructor(public dialogRef: MatDialogRef<ResendEmailDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) { }
closeDialog(email: string = ''): void {
this.dialogRef.close(email);
}
closeDialogWithSend(email: string = ''): void {
this.dialogRef.close({ send: true, email });
}
}

View File

@@ -1,69 +1,55 @@
<div class="method-col">
<div class="method-row">
<span class="label">{{ 'USER.PROFILE.PASSWORD' | translate }}</span>
<div class="left">
<span class="label">{{ 'USER.PROFILE.PASSWORD' | translate }}</span>
<span class="name">*********</span>
<span>*********</span>
<div class="overflow">
<ng-content select="[phoneAction]"></ng-content>
<a [disabled]="!canWrite" [routerLink]="['password']" mat-icon-button>
<mat-icon class="icon">chevron_right</mat-icon>
<ng-content select="[pwdAction]"></ng-content>
</div>
<div class="right">
<a matTooltip="{{'USER.PASSWORD.SET' | translate}}" [disabled]="!canWrite" [routerLink]="['password']"
mat-icon-button>
<i class="las la-edit"></i>
</a>
</div>
</div>
<div class="method-row">
<span class="label">{{ 'USER.EMAIL' | translate }}</span>
<div class="left">
<span class="label">{{ 'USER.EMAIL' | translate }}</span>
<span class="name">{{human?.email}}</span>
<span *ngIf="human?.isEmailVerified" class="state verified">{{'USER.EMAILVERIFIED' | translate}}</span>
<div *ngIf="!human?.isEmailVerified" class="block">
<span class="state notverified">{{'USER.NOTVERIFIED' | translate}}</span>
<ng-container *ngIf="!emailEditState; else emailEdit">
<div class="actions">
<span class="name">{{human?.email}}</span>
<mat-icon class="icon" *ngIf="human?.isEmailVerified" color="primary" aria-hidden="false"
aria-label="verified icon">
check_circle_outline</mat-icon>
<ng-container *ngIf="human?.email && !human?.isEmailVerified">
<mat-icon class="icon" color="warn" aria-hidden="false" aria-label="not verified icon">
highlight_off
</mat-icon>
<a *ngIf="canWrite" class="verify" matTooltip="{{'USER.LOGINMETHODS.EMAIL.RESEND' | translate}}"
<ng-container *ngIf="human?.email">
<a *ngIf="canWrite && state != userStateEnum?.USERSTATE_INITIAL" class="verify"
matTooltip="{{'USER.LOGINMETHODS.EMAIL.RESEND' | translate}}"
(click)="emitEmailVerification()">{{'USER.LOGINMETHODS.RESENDCODE' | translate}}</a>
</ng-container>
</div>
<div>
<button [disabled]="!canWrite" (click)="emailEditState = true" mat-icon-button>
<mat-icon class="icon">edit</mat-icon>
</button>
</div>
</ng-container>
<ng-template #emailEdit>
<mat-form-field class="name">
<mat-label>{{ 'USER.EMAIL' | translate }}</mat-label>
<input *ngIf="human && human.email !== undefined && human.email !== null" matInput
[(ngModel)]="human.email" />
</mat-form-field>
<button (click)="emailEditState = false" mat-icon-button>
<mat-icon class="icon">close</mat-icon>
<ng-content select="[emailAction]"></ng-content>
</div>
<div class="right">
<button [disabled]="!canWrite" (click)="openEditDialog(EditDialogType.EMAIL)" mat-icon-button>
<i class="las la-edit"></i>
</button>
<button *ngIf="human" [disabled]="!human.email" type="button" color="primary" (click)="saveEmail()"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</ng-template>
</div>
</div>
<div class="method-row">
<span class="label">{{ 'USER.PHONE' | translate }}</span>
<div class="left">
<span class="label">{{ 'USER.PHONE' | translate }}</span>
<span class="name">{{human?.phone ? human.phone : ('USER.PHONEEMPTY' | translate)}}</span>
<span *ngIf="human?.isPhoneVerified" class="state verified">{{'USER.PHONEVERIFIED' | translate}}</span>
<div *ngIf="!human?.isPhoneVerified" class="block">
<span class="state notverified">{{'USER.NOTVERIFIED' | translate}}</span>
<ng-container *ngIf="!phoneEditState; else phoneEdit">
<div class="actions">
<span class="name">{{human?.phone}}</span>
<mat-icon class="icon" *ngIf="human?.isPhoneVerified" color="primary" aria-hidden="false"
aria-label="verified icon">
check_circle_outline</mat-icon>
<ng-container *ngIf="human?.phone && !human?.isPhoneVerified">
<mat-icon class="icon" matTooltip="not verified" color="warn" aria-hidden="false"
aria-label="not verified icon">
highlight_off
</mat-icon>
<a *ngIf="!disablePhoneCode && !canWrite" class="verify"
<ng-container *ngIf="human?.phone">
<a *ngIf="!disablePhoneCode && canWrite" class="verify"
matTooltip="{{'USER.LOGINMETHODS.ENTERCODE_DESC' | translate}}"
(click)="enterCode()">{{'USER.LOGINMETHODS.ENTERCODE' | translate}}</a>
<a *ngIf="canWrite" class="verify" matTooltip="{{'USER.LOGINMETHODS.PHONE.RESEND' | translate}}"
@@ -71,27 +57,18 @@
</ng-container>
</div>
<div>
<button [disabled]="!canWrite" (click)="phoneEditState = true" mat-icon-button>
<mat-icon class="icon">edit</mat-icon>
</button>
</div>
</ng-container>
<ng-content select="[phoneAction]"></ng-content>
</div>
<ng-template #phoneEdit>
<mat-form-field class="name">
<mat-label>{{ 'USER.PHONE' | translate }}</mat-label>
<input *ngIf="human && human.phone !== undefined && human.phone !== null" matInput
[(ngModel)]="human.phone" />
</mat-form-field>
<button (click)="phoneEditState = false" mat-icon-button>
<mat-icon class="icon">close</mat-icon>
</button>
<button *ngIf="human && human.phone" color="warn" (click)="emitDeletePhone()" mat-icon-button>
<div class="right">
<button matTooltip="{{'ACTIONS.DELETE' | translate}}" *ngIf="human && human.phone" color="warn"
(click)="emitDeletePhone()" mat-icon-button>
<i class="las la-trash"></i>
</button>
<button *ngIf="human" [disabled]="!human.phone" type="button" color="primary" (click)="savePhone()"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</ng-template>
<button matTooltip="{{'ACTIONS.EDIT' | translate}}" [disabled]="!canWrite"
(click)="openEditDialog(EditDialogType.PHONE)" mat-icon-button>
<i class="las la-edit"></i>
</button>
</div>
</div>
</div>

View File

@@ -5,29 +5,57 @@
.method-row {
display: flex;
align-items: center;
justify-content: space-between;
align-items: center;
padding: .5rem;
border-bottom: 1px solid #ffffff20;
flex-wrap: wrap;
.actions {
flex: 1;
.left {
.label {
font-size: 13px;
margin-bottom: .5rem;
min-width: 100px;
color: var(--grey);
display: block;
}
.name {
display: block;
margin-bottom: .5rem;
}
.state {
font-size: 14px;
margin-bottom: .5rem;
&.verified {
color: #85d996;
display: block;
}
&.notverified {
color: #ff4436;
margin-right: 1rem;
}
}
.block {
display: block;
}
}
.right {
flex-basis: 70px;
display: flex;
justify-content: flex-end;
align-items: center;
flex-direction: column;
min-width: 150px;
}
.label {
font-size: .9rem;
max-width: 100px;
color: var(--grey);
}
.icon {
margin: .5rem;
.verified-icon {
font-size: 1.2rem;
line-height: 1.2rem;
height: 1.2rem;
cursor: default;
}
.verify {
@@ -38,6 +66,7 @@
cursor: pointer;
word-wrap: none;
white-space: nowrap;
margin-right: 1rem;
&:hover {
text-decoration: underline;
@@ -46,6 +75,6 @@
}
}
.overflow {
overflow: auto;
.mat-form-field-wrapper {
padding-bottom: 0 !important;
}

View File

@@ -1,46 +1,48 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { HumanView as AuthHumanView } from 'src/app/proto/generated/auth_pb';
import { HumanView as MgmtHumanView } from 'src/app/proto/generated/management_pb';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { HumanView as AuthHumanView, UserState as AuthUserState } from 'src/app/proto/generated/auth_pb';
import { HumanView as MgmtHumanView, UserState as MgmtUserState } from 'src/app/proto/generated/management_pb';
import { CodeDialogComponent } from '../auth-user-detail/code-dialog/code-dialog.component';
import { EditDialogType } from '../user-detail/user-detail.component';
@Component({
selector: 'app-contact',
templateUrl: './contact.component.html',
styleUrls: ['./contact.component.scss'],
})
export class ContactComponent implements OnInit {
export class ContactComponent {
@Input() disablePhoneCode: boolean = false;
@Input() canWrite: boolean = false;
@Input() human!: AuthHumanView.AsObject | MgmtHumanView.AsObject;
@Output() savedPhone: EventEmitter<string> = new EventEmitter();
@Output() savedEmail: EventEmitter<string> = new EventEmitter();
@Input() state!: AuthUserState | MgmtUserState;
@Output() editType: EventEmitter<EditDialogType> = new EventEmitter();
@Output() resendEmailVerification: EventEmitter<void> = new EventEmitter();
@Output() resendPhoneVerification: EventEmitter<void> = new EventEmitter();
@Output() enteredPhoneCode: EventEmitter<string> = new EventEmitter();
@Output() deletedPhone: EventEmitter<void> = new EventEmitter();
@Input() public userStateEnum: any;
public emailEditState: boolean = false;
public phoneEditState: boolean = false;
public EditDialogType: any = EditDialogType;
constructor(private dialog: MatDialog) { }
ngOnInit(): void {
}
savePhone(): void {
this.phoneEditState = false;
this.savedPhone.emit(this.human.phone);
}
emitDeletePhone(): void {
this.phoneEditState = false;
this.deletedPhone.emit();
}
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'USER.LOGINMETHODS.PHONE.DELETETITLE',
descriptionKey: 'USER.LOGINMETHODS.PHONE.DELETEDESC',
},
width: '400px',
});
saveEmail(): void {
this.emailEditState = false;
this.savedEmail.emit(this.human.email);
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
this.deletedPhone.emit();
}
});
}
emitEmailVerification(): void {
@@ -67,4 +69,8 @@ export class ContactComponent implements OnInit {
});
}
}
public openEditDialog(type: EditDialogType): void {
this.editType.emit(type);
}
}

View File

@@ -34,7 +34,10 @@ import { AuthUserDetailComponent } from './auth-user-detail/auth-user-detail.com
import { AuthUserMfaComponent } from './auth-user-detail/auth-user-mfa/auth-user-mfa.component';
import { CodeDialogComponent } from './auth-user-detail/code-dialog/code-dialog.component';
import { DialogOtpComponent } from './auth-user-detail/dialog-otp/dialog-otp.component';
import { EditDialogComponent } from './auth-user-detail/edit-dialog/edit-dialog.component';
import { ResendEmailDialogComponent } from './auth-user-detail/resend-email-dialog/resend-email-dialog.component';
import { ThemeSettingComponent } from './auth-user-detail/theme-setting/theme-setting.component';
import { ContactComponent } from './contact/contact.component';
import { DetailFormMachineModule } from './detail-form-machine/detail-form-machine.module';
import { DetailFormModule } from './detail-form/detail-form.module';
import { ExternalIdpsComponent } from './external-idps/external-idps.component';
@@ -46,13 +49,13 @@ import { PasswordComponent } from './password/password.component';
import { UserDetailRoutingModule } from './user-detail-routing.module';
import { UserDetailComponent } from './user-detail/user-detail.component';
import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
import { ContactComponent } from './contact/contact.component';
@NgModule({
declarations: [
AuthUserDetailComponent,
UserDetailComponent,
DialogOtpComponent,
EditDialogComponent,
AuthUserMfaComponent,
UserMfaComponent,
ThemeSettingComponent,
@@ -62,6 +65,7 @@ import { ContactComponent } from './contact/contact.component';
MachineKeysComponent,
ExternalIdpsComponent,
ContactComponent,
ResendEmailDialogComponent,
],
imports: [
UserDetailRoutingModule,

View File

@@ -67,15 +67,20 @@
<app-card *ngIf="user.human" title="{{ 'USER.LOGINMETHODS.TITLE' | translate }}"
description="{{ 'USER.LOGINMETHODS.DESCRIPTION' | translate }}">
<button card-actions mat-icon-button (click)="refreshUser()">
<mat-icon>refresh</mat-icon>
</button>
<app-contact disablePhoneCode="true"
[canWrite]="(['user.write:' + user?.id, 'user.write$'] | hasRole | async)" *ngIf="user?.human"
[human]="user.human" (savedPhone)="savePhone($event)" (savedEmail)="saveEmail($event)"
(deletedPhone)="deletePhone()" (resendEmailVerification)="resendEmailVerification()"
[human]="user.human" (editType)="openEditDialog($event)" (deletedPhone)="deletePhone()"
(resendEmailVerification)="resendEmailVerification()"
(resendPhoneVerification)="resendPhoneVerification()">
<button phoneAction [disabled]="(canWrite$ | async) == false" (click)="sendSetPasswordNotification()"
<button pwdAction [disabled]="(canWrite$ | async) == false" (click)="sendSetPasswordNotification()"
mat-stroked-button color="primary"
*ngIf="user.state === UserState.USERSTATE_INITIAL">{{ 'USER.PASSWORD.RESENDNOTIFICATION' | translate }}</button>
<button emailAction class="resendemail" *ngIf="user.state == UserState.USERSTATE_INITIAL"
mat-stroked-button color="primary"
(click)="resendInitEmail()">{{'USER.RESENDINITIALEMAIL' | translate}}</button>
</app-contact>
</app-card>

View File

@@ -1,9 +1,9 @@
import { Location } from '@angular/common';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { ChangeType } from 'src/app/modules/changes/changes.component';
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
@@ -21,18 +21,24 @@ import {
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { EditDialogComponent } from '../auth-user-detail/edit-dialog/edit-dialog.component';
import { ResendEmailDialogComponent } from '../auth-user-detail/resend-email-dialog/resend-email-dialog.component';
export enum EditDialogType {
PHONE = 1,
EMAIL = 2,
}
@Component({
selector: 'app-user-detail',
templateUrl: './user-detail.component.html',
styleUrls: ['./user-detail.component.scss'],
})
export class UserDetailComponent implements OnInit, OnDestroy {
export class UserDetailComponent implements OnInit {
public user!: UserView.AsObject;
public genders: Gender[] = [Gender.GENDER_MALE, Gender.GENDER_FEMALE, Gender.GENDER_DIVERSE];
public languages: string[] = ['de', 'en'];
private subscription: Subscription = new Subscription();
public ChangeType: any = ChangeType;
public loading: boolean = false;
@@ -40,6 +46,8 @@ export class UserDetailComponent implements OnInit, OnDestroy {
public copied: string = '';
public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.USER;
public EditDialogType: any = EditDialogType;
constructor(
public translate: TranslateService,
private route: ActivatedRoute,
@@ -49,8 +57,8 @@ export class UserDetailComponent implements OnInit, OnDestroy {
private dialog: MatDialog,
) { }
public ngOnInit(): void {
this.subscription = this.route.params.subscribe(params => {
refreshUser(): void {
this.route.params.pipe(take(1)).subscribe(params => {
const { id } = params;
this.mgmtUserService.GetUserByID(id).then(user => {
this.user = user.toObject();
@@ -60,8 +68,8 @@ export class UserDetailComponent implements OnInit, OnDestroy {
});
}
public ngOnDestroy(): void {
this.subscription.unsubscribe();
public ngOnInit(): void {
this.refreshUser();
}
public changeState(newState: UserState): void {
@@ -149,6 +157,7 @@ export class UserDetailComponent implements OnInit, OnDestroy {
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
if (this.user.human) {
this.user.human.phone = '';
this.refreshUser();
}
}).catch(error => {
this.toast.showError(error);
@@ -158,9 +167,10 @@ export class UserDetailComponent implements OnInit, OnDestroy {
public saveEmail(email: string): void {
if (this.user.id && email) {
this.mgmtUserService.SaveUserEmail(this.user.id, email).then((data: UserEmail) => {
this.toast.showInfo('USER.TOAST.EMAILSENT', true);
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
if (this.user.human) {
this.user.human.email = data.toObject().email;
this.refreshUser();
}
}).catch(error => {
this.toast.showError(error);
@@ -175,6 +185,7 @@ export class UserDetailComponent implements OnInit, OnDestroy {
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
if (this.user.human) {
this.user.human.phone = data.toObject().phone;
this.refreshUser();
}
}).catch(error => {
this.toast.showError(error);
@@ -217,4 +228,63 @@ export class UserDetailComponent implements OnInit, OnDestroy {
}
});
}
public resendInitEmail(): void {
const dialogRef = this.dialog.open(ResendEmailDialogComponent, {
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp.send && this.user.id) {
this.mgmtUserService.ResendInitialMail(this.user.id, resp.email ?? '').then(() => {
this.toast.showInfo('USER.TOAST.INITEMAILSENT', true);
}).catch(error => {
this.toast.showError(error);
});
}
});
}
public openEditDialog(type: EditDialogType): void {
switch (type) {
case EditDialogType.PHONE:
const dialogRefPhone = this.dialog.open(EditDialogComponent, {
data: {
confirmKey: 'ACTIONS.SAVE',
cancelKey: 'ACTIONS.CANCEL',
labelKey: 'ACTIONS.NEWVALUE',
titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC',
value: this.user.human?.phone,
},
width: '400px',
});
dialogRefPhone.afterClosed().subscribe(resp => {
if (resp) {
this.savePhone(resp);
}
});
break;
case EditDialogType.EMAIL:
const dialogRefEmail = this.dialog.open(EditDialogComponent, {
data: {
confirmKey: 'ACTIONS.SAVE',
cancelKey: 'ACTIONS.CANCEL',
labelKey: 'ACTIONS.NEWVALUE',
titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC',
value: this.user.human?.email,
},
width: '400px',
});
dialogRefEmail.afterClosed().subscribe(resp => {
if (resp) {
this.saveEmail(resp);
}
});
break;
}
}
}

View File

@@ -249,6 +249,12 @@ export class GrpcAuthService {
return this.grpcService.auth.changeMyUserEmail(req);
}
public ResendMyEmailVerificationMail(): Promise<Empty> {
return this.grpcService.auth.resendMyEmailVerificationMail(
new Empty(),
);
}
public RemoveMyUserPhone(): Promise<Empty> {
return this.grpcService.auth.removeMyUserPhone(
new Empty(),

View File

@@ -40,6 +40,7 @@ import {
IdpSearchResponse,
IdpUpdate,
IdpView,
InitialMailRequest,
LoginName,
LoginPolicy,
LoginPolicyRequest,
@@ -786,6 +787,16 @@ export class ManagementService {
return this.grpcService.mgmt.resendEmailVerificationMail(req);
}
public ResendInitialMail(userId: string, newemail: string): Promise<Empty> {
const req = new InitialMailRequest();
if (newemail) {
req.setEmail(newemail);
}
req.setId(userId);
return this.grpcService.mgmt.resendInitialMail(req);
}
public ResendPhoneVerification(id: string): Promise<any> {
const req = new UserID();
req.setId(id);

View File

@@ -77,7 +77,9 @@
"LOGIN":"Einloggen",
"EDIT":"Bearbeiten",
"PIN":"Anpinnen",
"CONFIGURE":"Konfigurieren"
"CONFIGURE":"Konfigurieren",
"SEND":"Senden",
"NEWVALUE":"Neuer Wert"
},
"ERRORS": {
"REQUIRED": "Bitte fülle alle benötigten Felder aus.",
@@ -112,6 +114,11 @@
"DELETE_TITLE":"User löschen",
"DELETE_DESCRIPTION":"Sie sind im Begriff einen Benutzer endgültig zu löschen. Wollen Sie dies wirklich tun?"
},
"SENDEMAILDIALOG":{
"TITLE":"Email Benachrichtigung senden",
"DESCRIPTION":"Klicken Sie den untenstehenden Button um ein verifizierungs Email an die aktuelle Adresse zu versenden oder ändern Sie die Emailadresse in dem Feld.",
"NEWEMAIL":"Neue Email"
},
"TABLE":{
"DEACTIVATE":"Deaktivieren",
"ACTIVATE":"Aktivieren",
@@ -241,6 +248,10 @@
},
"EMAIL": "E-Mail",
"PHONE": "Telefonnummer",
"PHONEEMPTY":"Keine Telefonnummer hinterlegt",
"PHONEVERIFIED":"Telefonnummer bestätigt.",
"EMAILVERIFIED":"Email verifiziert",
"NOTVERIFIED":"nicht verifiziert",
"PREFERRED_LOGINNAME":"Bevorzugter Loginname",
"LOGINMETHODS": {
"TITLE": "Kontaktinformationen",
@@ -248,12 +259,18 @@
"EMAIL": {
"TITLE": "E-Mail",
"VALID": "Validiert",
"RESEND": "Verifikationsmail erneut senden"
"RESEND": "Verifikationsmail erneut senden",
"EDITTITLE":"Email ändern",
"EDITDESC":"Geben Sie die neue Email in dem darunterliegenden Feld ein!"
},
"PHONE": {
"TITLE": "Telefon",
"VALID": "Validiert",
"RESEND": "Verifikationsnachricht erneut senden"
"RESEND": "Verifikationsnachricht erneut senden",
"EDITTITLE":"Nummer ändern",
"EDITDESC":"Geben Sie die neue Nummer in dem darunterliegenden Feld ein!",
"DELETETITLE":"Telefonnummer löschen",
"DELETEDESC":"Wollen Sie die Telefonnummer wirklich löschen?"
},
"RESENDCODE": "Code erneut senden",
"ENTERCODE":"Verifizieren",
@@ -294,10 +311,13 @@
"SIGNEDOUT_BTN":"Anmelden",
"EDITACCOUNT":"Konto bearbeiten",
"ADDACCOUNT":"Konto hinzufügen",
"RESENDINITIALEMAIL":"Neue Initialisierungsmail senden",
"RESENDEMAILNOTIFICATION":"Benachrichtigungsmail senden",
"TOAST": {
"CREATED":"Benutzer erfolgreich erstellt.",
"SAVED":"Profil gespeichert.",
"EMAILSAVED":"E-Mail gespeichert.",
"INITEMAILSENT":"Initialisierung Email gesendet.",
"PHONESAVED":"Telefonnummer gespeichert.",
"PHONEREMOVED":"Telefonnummer gelöscht.",
"PHONEVERIFIED":"Telefonnummer bestätigt.",

View File

@@ -77,7 +77,9 @@
"LOGIN":"Login",
"EDIT":"Edit",
"PIN":"Pin / Unpin",
"CONFIGURE":"Configure"
"CONFIGURE":"Configure",
"SEND":"Send",
"NEWVALUE":"New Value"
},
"ERRORS": {
"REQUIRED": "Some required fields are missing.",
@@ -112,6 +114,11 @@
"DELETE_TITLE":"Delete User",
"DELETE_DESCRIPTION":"You are about to permanently delete a user. Are you sure?"
},
"SENDEMAILDIALOG":{
"TITLE":"Send Email Notification",
"DESCRIPTION":"Click the button below to send a notification to the current email address or change the email address in the field.",
"NEWEMAIL":"New email address"
},
"TABLE":{
"DEACTIVATE":"Deactivate",
"ACTIVATE":"Activate",
@@ -240,7 +247,11 @@
"NOTEQUAL":"The passwords provided do not match."
},
"EMAIL": "E-mail",
"PHONE": "Phone Number",
"PHONE": "Phonenumber",
"PHONEEMPTY":"No phonenumber defined",
"PHONEVERIFIED":"Phonenumber verified.",
"EMAILVERIFIED":"Email verified",
"NOTVERIFIED":"not verified",
"PREFERRED_LOGINNAME":"Preferred Loginname",
"LOGINMETHODS": {
"TITLE": "Contact Information",
@@ -248,12 +259,18 @@
"EMAIL": {
"TITLE": "E-mail",
"VALID": "validated",
"RESEND": "Resend Verification E-mail"
"RESEND": "Resend Verification E-mail",
"EDITTITLE":"Change Email",
"EDITDESC":"Enter the new email in the field below."
},
"PHONE": {
"TITLE": "Phone",
"VALID": "validated",
"RESEND": "Resend Verification Text Message"
"RESEND": "Resend Verification Text Message",
"EDITTITLE":"Change number",
"EDITDESC":"Enter the new phonenumber in the field below.",
"DELETETITLE":"Delete Phonenumber",
"DELETEDESC":"Do you really want to delete the phonenumber"
},
"RESENDCODE": "Resend Code",
"ENTERCODE":"Verify",
@@ -294,10 +311,13 @@
"SIGNEDOUT_BTN":"Sign In",
"EDITACCOUNT":"Edit Account",
"ADDACCOUNT":"Log in With Another Account",
"RESENDINITIALEMAIL":"Send new initialisation mail",
"RESENDEMAILNOTIFICATION":"Resend Email notification",
"TOAST": {
"CREATED":"User created successfully.",
"SAVED":"Profile saved successfully.",
"EMAILSAVED":"E-mail saved successfully.",
"INITEMAILSENT":"Initializing mail sent.",
"PHONESAVED":"Phone saved successfully.",
"PHONEREMOVED":"Phone has been removed.",
"PHONEVERIFIED":"Phone verified successfully.",

View File

@@ -11,6 +11,7 @@
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&amp;display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Lato&display=swap" rel="stylesheet">
<link rel="stylesheet"
href="https://maxst.icons8.com/vue-static/landings/line-awesome/line-awesome/1.3.0/css/line-awesome.min.css">
<link rel="manifest" href="manifest.webmanifest">