feat: add confirmation field for user deletion, self warning (#2971)

This commit is contained in:
Max Peintner 2022-01-10 11:12:57 +01:00 committed by GitHub
parent e624047d60
commit 478beded9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 132 additions and 88 deletions

View File

@ -1,16 +1,22 @@
<span class="title" mat-dialog-title>{{data.titleKey | translate: data.titleParam}}</span> <span class="title" mat-dialog-title>{{data.titleKey | translate: data.titleParam}}</span>
<div mat-dialog-content> <div mat-dialog-content>
<div class="icon-wrapper" *ngIf="data.icon"> <div class="icon-wrapper" *ngIf="data.icon">
<i class="icon {{data.icon}}"></i> <i class="icon {{data.icon}}"></i>
</div> </div>
<p class="desc"> {{data.descriptionKey | translate: data.descriptionParam}}</p> <p class="desc"> {{data.descriptionKey | translate: data.descriptionParam}}</p>
<cnsl-form-field *ngIf="data.confirmation && data.confirmationKey" class="formfield">
<cnsl-label>{{data.confirmationKey | translate: {value: data.confirmation} }}</cnsl-label>
<input cnslInput [(ngModel)]="confirm" />
</cnsl-form-field>
</div> </div>
<div mat-dialog-actions class="action"> <div mat-dialog-actions class="action">
<button *ngIf="data.cancelKey" mat-button (click)="closeDialog()"> <button *ngIf="data.cancelKey" mat-button (click)="closeDialog()">
{{data.cancelKey | translate}} {{data.cancelKey | translate}}
</button> </button>
<span class="fill-space"></span> <span class="fill-space"></span>
<button color="warn" mat-raised-button class="ok-button" (click)="closeDialogWithSuccess()"> <button color="warn" [disabled]="data.confirmation && confirm !== data.confirmation" mat-raised-button
{{data.confirmKey | translate}} class="ok-button" (click)="closeDialogWithSuccess()">
{{data.confirmKey | translate}}
</button> </button>
</div> </div>

View File

@ -7,11 +7,8 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
styleUrls: ['./warn-dialog.component.scss'], styleUrls: ['./warn-dialog.component.scss'],
}) })
export class WarnDialogComponent { export class WarnDialogComponent {
public confirm: string = '';
constructor( constructor(public dialogRef: MatDialogRef<WarnDialogComponent>, @Inject(MAT_DIALOG_DATA) public data: any) {}
public dialogRef: MatDialogRef<WarnDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
) { }
public closeDialog(): void { public closeDialog(): void {
this.dialogRef.close(false); this.dialogRef.close(false);

View File

@ -1,18 +1,14 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { InputModule } from '../input/input.module';
import { WarnDialogComponent } from './warn-dialog.component'; import { WarnDialogComponent } from './warn-dialog.component';
@NgModule({ @NgModule({
declarations: [WarnDialogComponent], declarations: [WarnDialogComponent],
imports: [ imports: [CommonModule, FormsModule, TranslateModule, MatButtonModule, InputModule],
CommonModule,
TranslateModule,
MatButtonModule,
],
}) })
export class WarnDialogModule { } export class WarnDialogModule {}

View File

@ -23,6 +23,7 @@ import {
UserNameQuery, UserNameQuery,
UserState, UserState,
} from 'src/app/proto/generated/zitadel/user_pb'; } from 'src/app/proto/generated/zitadel/user_pb';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@ -38,9 +39,7 @@ enum UserListSearchKey {
selector: 'cnsl-user-table', selector: 'cnsl-user-table',
templateUrl: './user-table.component.html', templateUrl: './user-table.component.html',
styleUrls: ['./user-table.component.scss'], styleUrls: ['./user-table.component.scss'],
animations: [ animations: [enterAnimations],
enterAnimations,
],
}) })
export class UserTableComponent implements OnInit { export class UserTableComponent implements OnInit {
public userSearchKey: UserListSearchKey | undefined = undefined; public userSearchKey: UserListSearchKey | undefined = undefined;
@ -66,6 +65,7 @@ export class UserTableComponent implements OnInit {
constructor( constructor(
public translate: TranslateService, public translate: TranslateService,
private authService: GrpcAuthService,
private userService: ManagementService, private userService: ManagementService,
private toast: ToastService, private toast: ToastService,
private dialog: MatDialog, private dialog: MatDialog,
@ -77,7 +77,7 @@ export class UserTableComponent implements OnInit {
} }
ngOnInit(): void { ngOnInit(): void {
this.route.queryParams.pipe(take(1)).subscribe(params => { this.route.queryParams.pipe(take(1)).subscribe((params) => {
this.getData(10, 0, this.type); this.getData(10, 0, this.type);
if (params.deferredReload) { if (params.deferredReload) {
setTimeout(() => { setTimeout(() => {
@ -94,43 +94,48 @@ export class UserTableComponent implements OnInit {
} }
public masterToggle(): void { public masterToggle(): void {
this.isAllSelected() ? this.isAllSelected() ? this.selection.clear() : this.dataSource.data.forEach((row) => this.selection.select(row));
this.selection.clear() :
this.dataSource.data.forEach(row => this.selection.select(row));
} }
public changePage(event: PageEvent): void { public changePage(event: PageEvent): void {
this.selection.clear(); this.selection.clear();
this.getData(event.pageSize, event.pageIndex * event.pageSize, this.type); this.getData(event.pageSize, event.pageIndex * event.pageSize, this.type);
} }
public deactivateSelectedUsers(): void { public deactivateSelectedUsers(): void {
Promise.all(this.selection.selected.map(value => { Promise.all(
return this.userService.deactivateUser(value.id); this.selection.selected.map((value) => {
})).then(() => { return this.userService.deactivateUser(value.id);
this.toast.showInfo('USER.TOAST.SELECTEDDEACTIVATED', true); }),
this.selection.clear(); )
setTimeout(() => { .then(() => {
this.refreshPage(); this.toast.showInfo('USER.TOAST.SELECTEDDEACTIVATED', true);
}, 1000); this.selection.clear();
}).catch(error => { setTimeout(() => {
this.toast.showError(error); this.refreshPage();
}); }, 1000);
})
.catch((error) => {
this.toast.showError(error);
});
} }
public reactivateSelectedUsers(): void { public reactivateSelectedUsers(): void {
Promise.all(this.selection.selected.map(value => { Promise.all(
return this.userService.reactivateUser(value.id); this.selection.selected.map((value) => {
})).then(() => { return this.userService.reactivateUser(value.id);
this.toast.showInfo('USER.TOAST.SELECTEDREACTIVATED', true); }),
this.selection.clear(); )
setTimeout(() => { .then(() => {
this.refreshPage(); this.toast.showInfo('USER.TOAST.SELECTEDREACTIVATED', true);
}, 1000); this.selection.clear();
}).catch(error => { setTimeout(() => {
this.toast.showError(error); this.refreshPage();
}); }, 1000);
})
.catch((error) => {
this.toast.showError(error);
});
} }
private async getData(limit: number, offset: number, type: Type, searchValue?: string): Promise<void> { private async getData(limit: number, offset: number, type: Type, searchValue?: string): Promise<void> {
@ -180,21 +185,24 @@ export class UserTableComponent implements OnInit {
} }
} }
this.userService.listUsers(limit, offset, [query]).then(resp => { this.userService
if (resp.details?.totalResult) { .listUsers(limit, offset, [query])
this.totalResult = resp.details?.totalResult; .then((resp) => {
} else { if (resp.details?.totalResult) {
this.totalResult = 0; this.totalResult = resp.details?.totalResult;
} } else {
if (resp.details?.viewTimestamp) { this.totalResult = 0;
this.viewTimestamp = resp.details?.viewTimestamp; }
} if (resp.details?.viewTimestamp) {
this.dataSource.data = resp.resultList; this.viewTimestamp = resp.details?.viewTimestamp;
this.loadingSubject.next(false); }
}).catch(error => { this.dataSource.data = resp.resultList;
this.toast.showError(error); this.loadingSubject.next(false);
this.loadingSubject.next(false); })
}); .catch((error) => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
} }
public refreshPage(): void { public refreshPage(): void {
@ -205,12 +213,7 @@ export class UserTableComponent implements OnInit {
this.selection.clear(); this.selection.clear();
const filterValue = (event.target as HTMLInputElement).value; const filterValue = (event.target as HTMLInputElement).value;
this.getData( this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize, this.type, filterValue);
this.paginator.pageSize,
this.paginator.pageIndex * this.paginator.pageSize,
this.type,
filterValue,
);
} }
public setFilter(key: UserListSearchKey): void { public setFilter(key: UserListSearchKey): void {
@ -229,27 +232,57 @@ export class UserTableComponent implements OnInit {
} }
public deleteUser(user: User.AsObject): void { public deleteUser(user: User.AsObject): void {
const dialogRef = this.dialog.open(WarnDialogComponent, { const authUserData = {
data: { confirmKey: 'ACTIONS.DELETE',
confirmKey: 'ACTIONS.DELETE', cancelKey: 'ACTIONS.CANCEL',
cancelKey: 'ACTIONS.CANCEL', titleKey: 'USER.DIALOG.DELETE_SELF_TITLE',
titleKey: 'USER.DIALOG.DELETE_TITLE', descriptionKey: 'USER.DIALOG.DELETE_SELF_DESCRIPTION',
descriptionKey: 'USER.DIALOG.DELETE_DESCRIPTION', confirmationKey: 'USER.DIALOG.TYPEUSERNAME',
}, confirmation: user.preferredLoginName,
width: '400px', };
});
dialogRef.afterClosed().subscribe(resp => { const mgmtUserData = {
if (resp) { confirmKey: 'ACTIONS.DELETE',
this.userService.removeUser(user.id).then(() => { cancelKey: 'ACTIONS.CANCEL',
setTimeout(() => { titleKey: 'USER.DIALOG.DELETE_TITLE',
this.refreshPage(); descriptionKey: 'USER.DIALOG.DELETE_DESCRIPTION',
}, 1000); confirmationKey: 'USER.DIALOG.TYPEUSERNAME',
this.toast.showInfo('USER.TOAST.DELETED', true); confirmation: user.preferredLoginName,
}).catch(error => { };
this.toast.showError(error);
if (user && user.id) {
const authUser = this.authService.userSubject.getValue();
const isMe = authUser?.id === user.id;
let dialogRef;
if (isMe) {
dialogRef = this.dialog.open(WarnDialogComponent, {
data: authUserData,
width: '400px',
});
} else {
dialogRef = this.dialog.open(WarnDialogComponent, {
data: mgmtUserData,
width: '400px',
}); });
} }
});
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.userService
.removeUser(user.id)
.then(() => {
setTimeout(() => {
this.refreshPage();
}, 1000);
this.toast.showInfo('USER.TOAST.DELETED', true);
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
} }
} }

View File

@ -96,6 +96,7 @@ import { StorageKey, StorageLocation, StorageService } from './storage.service';
export class GrpcAuthService { export class GrpcAuthService {
private _activeOrgChanged: Subject<Org.AsObject> = new Subject(); private _activeOrgChanged: Subject<Org.AsObject> = new Subject();
public user!: Observable<User.AsObject | undefined>; public user!: Observable<User.AsObject | undefined>;
public userSubject: BehaviorSubject<User.AsObject | undefined> = new BehaviorSubject<User.AsObject | undefined>(undefined);
private zitadelPermissions: BehaviorSubject<string[]> = new BehaviorSubject(['user.resourceowner']); private zitadelPermissions: BehaviorSubject<string[]> = new BehaviorSubject(['user.resourceowner']);
private zitadelFeatures: BehaviorSubject<string[]> = new BehaviorSubject(['']); private zitadelFeatures: BehaviorSubject<string[]> = new BehaviorSubject(['']);
@ -137,6 +138,8 @@ export class GrpcAuthService {
}), }),
); );
this.user.subscribe(this.userSubject);
this.activeOrgChanged.subscribe(() => { this.activeOrgChanged.subscribe(() => {
this.loadPermissions(); this.loadPermissions();
this.loadFeatures(); this.loadFeatures();

View File

@ -194,8 +194,11 @@
}, },
"DIALOG": { "DIALOG": {
"DELETE_TITLE": "User löschen", "DELETE_TITLE": "User löschen",
"DELETE_SELF_TITLE": "Eigenen User löschen",
"DELETE_DESCRIPTION": "Sie sind im Begriff einen Benutzer endgültig zu löschen. Wollen Sie dies wirklich tun?", "DELETE_DESCRIPTION": "Sie sind im Begriff einen Benutzer endgültig zu löschen. Wollen Sie dies wirklich tun?",
"DELETE_SELF_DESCRIPTION": "Sie sind im Begriff Ihren eigenen Benutzer endgültig zu löschen. Dadurch werden Sie ausgeloggt und Ihr Account wird gelöscht. Diese Aktion kann nicht rückgängig gemacht werden!",
"DELETE_AUTH_DESCRIPTION": "Sie sind im Begriff Ihren Account endgültig zu löschen. Wollen Sie dies wirklich tun?", "DELETE_AUTH_DESCRIPTION": "Sie sind im Begriff Ihren Account endgültig zu löschen. Wollen Sie dies wirklich tun?",
"TYPEUSERNAME": "Wiederholen Sie '{{value}}', um den Benutzer zu löschen.",
"DELETE_BTN": "Entgültig löschen" "DELETE_BTN": "Entgültig löschen"
}, },
"SENDEMAILDIALOG": { "SENDEMAILDIALOG": {

View File

@ -194,8 +194,11 @@
}, },
"DIALOG": { "DIALOG": {
"DELETE_TITLE": "Delete User", "DELETE_TITLE": "Delete User",
"DELETE_SELF_TITLE": "Delete Account",
"DELETE_DESCRIPTION": "You are about to permanently delete a user. Are you sure?", "DELETE_DESCRIPTION": "You are about to permanently delete a user. Are you sure?",
"DELETE_SELF_DESCRIPTION": "You are about to permanently delete your personal account. This will log you out and delete your user. This action cannot be undone!",
"DELETE_AUTH_DESCRIPTION": "You are about to permanently delete your personal account. Are you sure?", "DELETE_AUTH_DESCRIPTION": "You are about to permanently delete your personal account. Are you sure?",
"TYPEUSERNAME": "Type '{{value}}', to confirm and delete the user.",
"DELETE_BTN": "Delete permanently" "DELETE_BTN": "Delete permanently"
}, },
"SENDEMAILDIALOG": { "SENDEMAILDIALOG": {

View File

@ -194,8 +194,11 @@
}, },
"DIALOG": { "DIALOG": {
"DELETE_TITLE": "Elimina utente", "DELETE_TITLE": "Elimina utente",
"DELETE_SELF_TITLE": "Elimina Account",
"DELETE_DESCRIPTION": "Stai per eliminare definitivamente un utente. Sei sicuro?", "DELETE_DESCRIPTION": "Stai per eliminare definitivamente un utente. Sei sicuro?",
"DELETE_SELF_DESCRIPTION": "Stai per eliminare definitivamente il tuo account. Questo ti disconnetterà ed eliminerà il tuo utente. Questa azione non può essere annullata!",
"DELETE_AUTH_DESCRIPTION": "Stai per eleminare il tuo account personale in modo permanente. Vuoi continuare?", "DELETE_AUTH_DESCRIPTION": "Stai per eleminare il tuo account personale in modo permanente. Vuoi continuare?",
"TYPEUSERNAME": "Ripeti '{{value}}' per confermare ed eliminare l'utente.",
"DELETE_BTN": "Elimina" "DELETE_BTN": "Elimina"
}, },
"SENDEMAILDIALOG": { "SENDEMAILDIALOG": {