Skip to content

Commit f9528ea

Browse files
sarkapalkovicovaHejdaJakub
authored andcommitted
feat(admin): blocked logins page
- Added page with blocked logins for perun admin. - Sort logins, filter by namespace and search by login. - Export displayed/all data. - Dialogs for un/blocking logins.
1 parent f78896e commit f9528ea

File tree

43 files changed

+2111
-378
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2111
-378
lines changed

apps/admin-gui/src/app/admin/admin-routing.module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { AdminConsentHubsComponent } from './pages/admin-page/admin-consent-hubs
3434
import { AdminSearcherComponent } from './pages/admin-page/admin-searcher/admin-searcher.component';
3535
import { RouteAuthGuardService } from '../shared/route-auth-guard.service';
3636
import { UserBansComponent } from '../users/pages/user-detail-page/user-bans/user-bans.component';
37+
import { AdminBlockedLoginsComponent } from './pages/admin-page/admin-blocked-logins/admin-blocked-logins.component';
3738

3839
const routes: Routes = [
3940
{
@@ -112,6 +113,11 @@ const routes: Routes = [
112113
component: AdminSearcherComponent,
113114
data: { animation: 'AdminSearcherPage' },
114115
},
116+
{
117+
path: 'blocked_logins',
118+
component: AdminBlockedLoginsComponent,
119+
data: { animation: 'AdminBlockedLoginsPage' },
120+
},
115121
],
116122
},
117123
{

apps/admin-gui/src/app/admin/admin.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { AdminOwnersComponent } from './pages/admin-page/admin-owners/admin-owne
2929
import { AdminAuditLogComponent } from './pages/admin-page/admin-audit-log/admin-audit-log.component';
3030
import { AdminConsentHubsComponent } from './pages/admin-page/admin-consent-hubs/admin-consent-hubs.component';
3131
import { AdminSearcherComponent } from './pages/admin-page/admin-searcher/admin-searcher.component';
32+
import { AdminBlockedLoginsComponent } from './pages/admin-page/admin-blocked-logins/admin-blocked-logins.component';
3233

3334
@NgModule({
3435
declarations: [
@@ -52,6 +53,7 @@ import { AdminSearcherComponent } from './pages/admin-page/admin-searcher/admin-
5253
AdminAuditLogComponent,
5354
AdminConsentHubsComponent,
5455
AdminSearcherComponent,
56+
AdminBlockedLoginsComponent,
5557
],
5658
imports: [
5759
NgxGraphModule,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<h1 class="page-subtitle">{{'ADMIN.BLOCKED_LOGINS.TITLE' | translate}}</h1>
2+
3+
<div class="align-elements">
4+
<div>
5+
<perun-web-apps-refresh-button (refresh)="refreshTable()"></perun-web-apps-refresh-button>
6+
<button
7+
(click)="block()"
8+
color="accent"
9+
class="me-2 action-button"
10+
mat-flat-button
11+
*ngIf="isAdmin">
12+
{{'ADMIN.BLOCKED_LOGINS.BLOCK' | translate}}
13+
</button>
14+
<button
15+
*ngIf="isAdmin"
16+
(click)="unblock()"
17+
class="me-2"
18+
color="warn"
19+
mat-flat-button
20+
[disabled]="selection.selected.length === 0">
21+
{{'ADMIN.BLOCKED_LOGINS.UNBLOCK' | translate}}
22+
</button>
23+
</div>
24+
25+
<perun-web-apps-namespace-search-select
26+
class="pr-2 me-2 flex-grow-1"
27+
[namespaceOptions]="filterOptions"
28+
[multiple]="true"
29+
(namespaceSelected)="toggleEvent($event)"
30+
[disableAutoSelect]="true"
31+
[customSelectPlaceholder]="'ADMIN.BLOCKED_LOGINS.FILTER_NAMESPACE' | translate"
32+
(selectClosed)="refreshOnClosed()">
33+
</perun-web-apps-namespace-search-select>
34+
35+
<perun-web-apps-debounce-filter
36+
class="search-field flex-grow-1"
37+
[autoFocus]="true"
38+
[placeholder]="'ADMIN.BLOCKED_LOGINS.SEARCH_PLACEHOLDER'"
39+
(filter)="onSearchByString($event)">
40+
</perun-web-apps-debounce-filter>
41+
</div>
42+
43+
<ng-template #spinner>
44+
<perun-web-apps-loading-table></perun-web-apps-loading-table>
45+
</ng-template>
46+
<div class="position-relative">
47+
<perun-web-apps-blocked-logins-dynamic-list
48+
*perunWebAppsLoader="loading$ | async; indicator: spinner"
49+
(loading$)="loading$ = $event"
50+
[searchString]="searchString"
51+
[tableId]="tableId"
52+
[updateTable]="update"
53+
[selection]="selection"
54+
[selectedNamespaces]="selectedNamespaces">
55+
</perun-web-apps-blocked-logins-dynamic-list>
56+
</div>

apps/admin-gui/src/app/admin/pages/admin-page/admin-blocked-logins/admin-blocked-logins.component.scss

Whitespace-only changes.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
2+
import { BlockedLogin } from '@perun-web-apps/perun/openapi';
3+
import { SelectionModel } from '@angular/cdk/collections';
4+
import { Observable, of } from 'rxjs';
5+
import { TABLE_ADMIN_BLOCKED_LOGINS } from '@perun-web-apps/config/table-config';
6+
import { getDefaultDialogConfig } from '@perun-web-apps/perun/utils';
7+
import { MatDialog } from '@angular/material/dialog';
8+
import { UnblockLoginsDialogComponent } from '../../../../shared/components/dialogs/unblock-logins-dialog/unblock-logins-dialog.component';
9+
import { BlockLoginsDialogComponent } from '../../../../shared/components/dialogs/block-logins-dialog/block-logins-dialog.component';
10+
import { AttributesManagerService } from '@perun-web-apps/perun/openapi';
11+
import { FormControl } from '@angular/forms';
12+
import { GuiAuthResolver } from '@perun-web-apps/perun/services';
13+
14+
@Component({
15+
selector: 'app-perun-web-apps-admin-blocked-logins',
16+
templateUrl: './admin-blocked-logins.component.html',
17+
styleUrls: ['./admin-blocked-logins.component.scss'],
18+
})
19+
export class AdminBlockedLoginsComponent implements OnInit {
20+
loading$: Observable<boolean>;
21+
update = false;
22+
tableId = TABLE_ADMIN_BLOCKED_LOGINS;
23+
isAdmin = false;
24+
25+
searchString: string;
26+
selection = new SelectionModel<BlockedLogin>(true, []);
27+
28+
logins: BlockedLogin[] = [];
29+
30+
namespaceOptions: string[] = [];
31+
filterOptions: string[] = [];
32+
selectedNamespaces: string[] = [];
33+
namespaces = new FormControl();
34+
35+
constructor(
36+
private cd: ChangeDetectorRef,
37+
private dialog: MatDialog,
38+
private attributesService: AttributesManagerService,
39+
public authResolver: GuiAuthResolver
40+
) {}
41+
42+
refreshTable(): void {
43+
this.update = !this.update;
44+
this.cd.detectChanges();
45+
}
46+
47+
onSearchByString(searchString: string): void {
48+
this.searchString = searchString;
49+
this.cd.detectChanges();
50+
}
51+
52+
ngOnInit(): void {
53+
this.loading$ = of(true);
54+
this.namespaces.setValue(this.selectedNamespaces);
55+
this.isAdmin = this.authResolver.isPerunAdmin();
56+
57+
this.attributesService.getAllNamespaces().subscribe((res) => {
58+
this.namespaceOptions = res;
59+
this.filterOptions = [''].concat(res);
60+
});
61+
}
62+
63+
block(): void {
64+
const config = getDefaultDialogConfig();
65+
config.width = '450px';
66+
config.data = {
67+
theme: 'admin-theme',
68+
namespaceOptions: this.namespaceOptions,
69+
};
70+
71+
const dialogRef = this.dialog.open(BlockLoginsDialogComponent, config);
72+
73+
dialogRef.afterClosed().subscribe((wereLoginsBlocked) => {
74+
if (wereLoginsBlocked) {
75+
this.update = !this.update;
76+
this.selection.clear();
77+
this.cd.detectChanges();
78+
}
79+
});
80+
}
81+
82+
unblock(): void {
83+
const config = getDefaultDialogConfig();
84+
config.width = '650px';
85+
config.data = {
86+
logins: this.selection.selected,
87+
theme: 'admin-theme',
88+
};
89+
90+
const dialogRef = this.dialog.open(UnblockLoginsDialogComponent, config);
91+
92+
dialogRef.afterClosed().subscribe((wereLoginsUnblocked) => {
93+
if (wereLoginsUnblocked) {
94+
this.update = !this.update;
95+
this.selection.clear();
96+
this.cd.detectChanges();
97+
}
98+
});
99+
}
100+
101+
toggleEvent(namespaces: string[]): void {
102+
// Replace array in-place so it won't trigger ngOnChanges
103+
this.selectedNamespaces.splice(
104+
0,
105+
this.selectedNamespaces.length,
106+
...namespaces.map((namespace) => (namespace === '' ? null : namespace))
107+
);
108+
}
109+
110+
refreshOnClosed(): void {
111+
this.selectedNamespaces = [...this.selectedNamespaces];
112+
this.cd.detectChanges();
113+
}
114+
}

apps/admin-gui/src/app/admin/pages/admin-page/admin-overview/admin-overview.component.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,11 @@ export class AdminOverviewComponent {
6464
label: 'MENU_ITEMS.ADMIN.SEARCHER',
6565
style: 'admin-btn',
6666
},
67+
{
68+
cssIcon: 'perun-blocked-logins',
69+
url: '/admin/blocked_logins',
70+
label: 'MENU_ITEMS.ADMIN.BLOCKED_LOGINS',
71+
style: 'admin-btn',
72+
},
6773
];
6874
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<ng-template #spinner>
2+
<perun-web-apps-loading-dialog></perun-web-apps-loading-dialog>
3+
</ng-template>
4+
<div class="{{data.theme}} position-relative">
5+
<div *perunWebAppsLoader="loading; indicator: spinner">
6+
<h1 mat-dialog-title>{{'DIALOGS.BLOCK_LOGINS.TITLE' | translate}}</h1>
7+
8+
<div class="dialog-container" mat-dialog-content>
9+
<mat-radio-group [(ngModel)]="isGlobal" class="d-flex flex-column">
10+
<mat-radio-button class="me-3" color="primary" [value]="true" [checked]="isGlobal">
11+
{{'DIALOGS.BLOCK_LOGINS.GLOBAL' | translate}}
12+
</mat-radio-button>
13+
<mat-radio-button
14+
class="me-3"
15+
color="primary"
16+
[value]="false"
17+
[checked]="!isGlobal"
18+
*ngIf="data.namespaceOptions.length > 0">
19+
{{'DIALOGS.BLOCK_LOGINS.SPECIFIC' | translate}}
20+
</mat-radio-button>
21+
</mat-radio-group>
22+
23+
<perun-web-apps-namespace-search-select
24+
*ngIf="data.namespaceOptions.length > 0 && !isGlobal"
25+
[namespaceOptions]="data.namespaceOptions"
26+
[disableDeselectButton]="true"
27+
[disableAutoSelect]="true"
28+
[customFindPlaceholder]="'DIALOGS.BLOCK_LOGINS.FIND_PLACEHOLDER'"
29+
(namespaceSelected)="selectedNamespace = $event">
30+
</perun-web-apps-namespace-search-select>
31+
32+
<mat-form-field class="pt-2 d-flex flex-column">
33+
<mat-label>{{'DIALOGS.BLOCK_LOGINS.INSERT_HERE'| translate}}</mat-label>
34+
<textarea
35+
cols="50"
36+
class="md-textarea form-control"
37+
[formControl]="blockLogins"
38+
required
39+
matInput
40+
placeholder="{{'DIALOGS.BLOCK_LOGINS.PLACEHOLDER' | translate}}"
41+
rows="8"></textarea>
42+
<mat-error *ngIf="blockLogins.hasError('required')">
43+
{{'DIALOGS.BLOCK_LOGINS.LOGINS_ERROR' | translate}}
44+
</mat-error>
45+
</mat-form-field>
46+
</div>
47+
48+
<div mat-dialog-actions>
49+
<button (click)="onCancel()" class="ms-auto" mat-flat-button>
50+
{{'DIALOGS.BULK_INVITE_MEMBERS.CANCEL' | translate}}
51+
</button>
52+
<button
53+
(click)="onSubmit()"
54+
class="ms-2"
55+
color="accent"
56+
[disabled]="loading || blockLogins.invalid || (!isGlobal && selectedNamespace === null)"
57+
mat-flat-button>
58+
{{'DIALOGS.BLOCK_LOGINS.BLOCK' | translate}}
59+
</button>
60+
</div>
61+
</div>
62+
</div>

apps/admin-gui/src/app/shared/components/dialogs/block-logins-dialog/block-logins-dialog.component.scss

Whitespace-only changes.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Component, Inject } from '@angular/core';
2+
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
3+
import { NotificatorService, StoreService } from '@perun-web-apps/perun/services';
4+
import { FormControl, Validators } from '@angular/forms';
5+
import { UsersManagerService } from '@perun-web-apps/perun/openapi';
6+
7+
export interface BlockLoginsDialogData {
8+
theme: string;
9+
namespaceOptions: string[];
10+
}
11+
12+
@Component({
13+
selector: 'app-block-logins-dialog',
14+
templateUrl: './block-logins-dialog.component.html',
15+
styleUrls: ['./block-logins-dialog.component.scss'],
16+
})
17+
export class BlockLoginsDialogComponent {
18+
loading = false;
19+
blockLogins = new FormControl('', Validators.required);
20+
namespace = new FormControl('', Validators.required);
21+
isGlobal = true;
22+
selectedNamespace: string | null = null;
23+
24+
constructor(
25+
public dialogRef: MatDialogRef<BlockLoginsDialogComponent>,
26+
@Inject(MAT_DIALOG_DATA) public data: BlockLoginsDialogData,
27+
private store: StoreService,
28+
private usersService: UsersManagerService,
29+
private notificator: NotificatorService
30+
) {}
31+
32+
onCancel(): void {
33+
this.dialogRef.close(false);
34+
}
35+
36+
onSubmit(): void {
37+
this.loading = true;
38+
this.usersService
39+
.blockLogins(
40+
this.blockLogins.value.split('\n').map((login) => login.trim()),
41+
this.isGlobal ? null : this.selectedNamespace
42+
)
43+
.subscribe({
44+
next: () => {
45+
this.notificator.showInstantSuccess('ADMIN.BLOCKED_LOGINS.BLOCK_SUCCESS');
46+
this.dialogRef.close(true);
47+
this.loading = false;
48+
},
49+
error: () => {
50+
this.loading = false;
51+
},
52+
});
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<ng-template #spinner>
2+
<perun-web-apps-loading-dialog></perun-web-apps-loading-dialog>
3+
</ng-template>
4+
<div class="{{theme}} position-relative">
5+
<div *perunWebAppsLoader="loading; indicator: spinner">
6+
<h1 mat-dialog-title>{{'DIALOGS.UNBLOCK_LOGINS.TITLE' | translate}}</h1>
7+
<div mat-dialog-content>
8+
<p>
9+
{{'DIALOGS.UNBLOCK_LOGINS.DESCRIPTION' | translate}}
10+
</p>
11+
12+
<div class="fw-bold">
13+
{{'DIALOGS.UNBLOCK_LOGINS.ASK' | translate}}
14+
</div>
15+
16+
<table [dataSource]="dataSource" class="w-100" mat-table>
17+
<ng-container matColumnDef="id">
18+
<th *matHeaderCellDef mat-header-cell></th>
19+
<td *matCellDef="let blockedLogin" mat-cell>{{blockedLogin.id}}</td>
20+
</ng-container>
21+
<ng-container matColumnDef="login">
22+
<th *matHeaderCellDef mat-header-cell></th>
23+
<td *matCellDef="let blockedLogin" mat-cell class="trim-login">
24+
{{blockedLogin.login}}
25+
</td>
26+
</ng-container>
27+
<ng-container matColumnDef="namespace">
28+
<th *matHeaderCellDef mat-header-cell></th>
29+
<td
30+
*matCellDef="let blockedLogin"
31+
mat-cell
32+
class="{{!blockedLogin.namespace ? 'fst-italic' : ''}}">
33+
{{blockedLogin.namespace | globalNamespace}}
34+
</td>
35+
</ng-container>
36+
37+
<tr *matHeaderRowDef="displayedColumns" class="fw-bolder" mat-header-row></tr>
38+
<tr *matRowDef="let blockedLogin; columns: displayedColumns;" mat-row></tr>
39+
</table>
40+
</div>
41+
<div mat-dialog-actions>
42+
<button (click)="onCancel()" class="ms-auto" mat-flat-button>
43+
{{'DIALOGS.UNBLOCK_LOGINS.CANCEL' | translate}}
44+
</button>
45+
<button (click)="onSubmit()" class="ms-2" color="warn" mat-flat-button>
46+
{{'DIALOGS.UNBLOCK_LOGINS.UNBLOCK' | translate}}
47+
</button>
48+
</div>
49+
</div>
50+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.trim-login {
2+
overflow: hidden;
3+
text-overflow: ellipsis;
4+
max-width: 250px;
5+
}

0 commit comments

Comments
 (0)