From b6052e2de7467b537a006e0392b33b59834028dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 15 Jun 2025 20:14:52 +0200 Subject: [PATCH 01/16] Include 403 as special status code --- .../components/loading-error/loading-error.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Turnierplan.App/Client/src/app/portal/components/loading-error/loading-error.component.html b/src/Turnierplan.App/Client/src/app/portal/components/loading-error/loading-error.component.html index 9f04657b..4a24f827 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/loading-error/loading-error.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/components/loading-error/loading-error.component.html @@ -1,4 +1,4 @@ -@if (statusCode === 404) { +@if (statusCode === 403 || statusCode === 404) { +
+ +
+ +
+ + +
+ @if (isLoadingRoleAssignments) { +
+ + +
+ } @else { +
+ + {{ roleAssignmentCount }} +
+ + + + @for (group of roleAssignments | keyvalue; track group.key) { + + + + + @for (assignment of group.value; track assignment.id) { + + + + } + } + +
+ + ({{ group.value.length }}) +
+
+
+ @switch (assignment.principal.kind) { + @case (PrincipalKind.ApiKey) { + + } + @case (PrincipalKind.User) { + + } + } + + {{ assignment.principal.objectId }} + @if (canDeleteAssignment(assignment)) { + + + } +
+ @if (assignment.isInherited) { +
+ + + + {{ assignment.scope }} +
+ } @else { +
+ + +
+ } +
+ + {{ assignment.id }} + · + + {{ assignment.createdAt | translateDate }} +
+
+
+ } +
+
diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.ts b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.ts new file mode 100644 index 00000000..eb728eda --- /dev/null +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.ts @@ -0,0 +1,88 @@ +import { Component, EventEmitter, Input, OnInit, Output, TemplateRef } from '@angular/core'; +import { NgbOffcanvas, NgbOffcanvasRef } from '@ng-bootstrap/ng-bootstrap'; +import { PrincipalKind, Role, RoleAssignmentDto, RoleAssignmentsService } from '../../../api'; +import { finalize } from 'rxjs'; + +export interface IRbacWidgetTarget { + rbacScopeId: string; +} + +@Component({ + standalone: false, + selector: 'tp-rbac-widget', + templateUrl: './rbac-widget.component.html' +}) +export class RbacWidgetComponent { + @Input() + public translationKey: string = ''; + + @Input() + public target!: IRbacWidgetTarget; + + @Input() + public targetIsOrganization: boolean = false; + + @Output() + public errorOccured = new EventEmitter(); + + protected readonly PrincipalKind = PrincipalKind; + + protected isLoadingRoleAssignments = false; + protected roleAssignments: { [key: string]: RoleAssignmentDto[] } = {}; + protected roleAssignmentCount: number = 0; + protected currentOffcanvas?: NgbOffcanvasRef; + + constructor( + private readonly offcanvasService: NgbOffcanvas, + private readonly roleAssignmentsService: RoleAssignmentsService + ) {} + + protected buttonClicked(template: TemplateRef): void { + this.loadRoleAssignments(); + this.currentOffcanvas = this.offcanvasService.open(template, { position: 'end' }); + } + + private loadRoleAssignments(): void { + this.isLoadingRoleAssignments = true; + + this.roleAssignmentsService + .getRoleAssignments({ scopeId: this.target.rbacScopeId }) + .pipe(finalize(() => (this.isLoadingRoleAssignments = false))) + .subscribe({ + next: (roleAssignments) => { + this.roleAssignments = {}; + this.roleAssignmentCount = 0; + + for (const roleAssignment of roleAssignments) { + this.roleAssignmentCount++; + + if (roleAssignment.role in this.roleAssignments) { + this.roleAssignments[roleAssignment.role].push(roleAssignment); + } else { + this.roleAssignments[roleAssignment.role] = [roleAssignment]; + } + } + }, + error: (error) => { + this.errorOccured.emit(error); + } + }); + } + + protected removeRoleAssignment(id: string): void { + // TODO: Implement this method + } + + protected canDeleteAssignment(assignment: RoleAssignmentDto): boolean { + if (assignment.isInherited) { + return false; + } + + if (assignment.role === Role.Owner && this.targetIsOrganization && this.roleAssignments[Role.Owner].length === 1) { + // This is a special case which forbids deleting the only Owner assignment from an organization. + return false; + } + + return true; + } +} diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.html b/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.html index 9c216460..1990ea2c 100644 --- a/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.html @@ -166,6 +166,14 @@ } } @case (3) { +
+ +
+

+
+ +
+
+
+ +
+
Date: Mon, 16 Jun 2025 17:31:39 +0200 Subject: [PATCH 03/16] Add another TODO --- .../app/portal/components/rbac-widget/rbac-widget.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html index 3d814901..a925b93c 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html @@ -56,6 +56,7 @@
+ {{ assignment.scope }}
From 93676f5d3fd6b1e5e040dd3ec7f0e17c47a6a63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Mon, 16 Jun 2025 17:45:15 +0200 Subject: [PATCH 04/16] Fix api key creation --- src/Turnierplan.App/Endpoints/ApiKeys/CreateApiKeyEndpoint.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Turnierplan.App/Endpoints/ApiKeys/CreateApiKeyEndpoint.cs b/src/Turnierplan.App/Endpoints/ApiKeys/CreateApiKeyEndpoint.cs index 39226a89..6a51ce69 100644 --- a/src/Turnierplan.App/Endpoints/ApiKeys/CreateApiKeyEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/ApiKeys/CreateApiKeyEndpoint.cs @@ -29,6 +29,7 @@ private static async Task Handle( IAccessValidator accessValidator, IPasswordHasher secretHasher, IApiKeyRepository apiKeyRepository, + IRoleAssignmentRepository organizationRoleAssignmentRepository, IMapper mapper, CancellationToken cancellationToken) { @@ -62,8 +63,9 @@ private static async Task Handle( await apiKeyRepository.UnitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); // Once the api key has an ID, the role assignment can be created - organization.AddRoleAssignment(Role.Reader, apiKey.AsPrincipal()); + var roleAssignment = organization.AddRoleAssignment(Role.Reader, apiKey.AsPrincipal()); + await organizationRoleAssignmentRepository.CreateAsync(roleAssignment).ConfigureAwait(false); await apiKeyRepository.UnitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); transaction.ShouldCommit = true; From 7693b97f854ff5bc3cba96940ce282c421e112dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Mon, 16 Jun 2025 17:49:16 +0200 Subject: [PATCH 05/16] Add role description --- src/Turnierplan.App/Client/src/app/i18n/de.ts | 9 +++++++-- .../components/rbac-widget/rbac-widget.component.html | 6 +++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Turnierplan.App/Client/src/app/i18n/de.ts b/src/Turnierplan.App/Client/src/app/i18n/de.ts index 39cf10a5..a69301f1 100644 --- a/src/Turnierplan.App/Client/src/app/i18n/de.ts +++ b/src/Turnierplan.App/Client/src/app/i18n/de.ts @@ -923,11 +923,16 @@ export const de = { ButtonLabel: 'Verwalten', Loading: 'Rollenzuweisungen werden geladen', NumberOfAssignments: 'Anzahl Rollenzuweisungen:', - RoleNames: { + RoleName: { Owner: 'Besitzer', Contributor: 'Mitwirkender', Reader: 'Leser' }, + RoleDescription: { + Owner: 'Der Benutzer kann sämtliche Änderungen durchführen inkl. Änderungen an Zugriffsrechten.', + Contributor: 'Der Benutzer kann sämtliche Änderungen durchführen ausgenommen Änderungen an Zugriffsrechten.', + Reader: 'Der Benutzer kann sämtliche Informationen lesen aber keine Änderungen durchführen.' + }, PrincipalKind: { ApiKey: 'API-Schlüssel', User: 'Benutzer' @@ -935,7 +940,7 @@ export const de = { Id: 'ID:', CreatedAt: 'Erstellt am:', NotInherited: 'Zuweisung stammt von dieser Resource', - Inherited: 'Vererbt von:', + Inherited: 'Vererbt durch:', InheritedTooltip: 'Diese Rollenzuweisung existiert implizit aufgrund der Zugehörigkeit zu einer anderen Resource' } } diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html index a925b93c..f6fe5892 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html @@ -22,13 +22,13 @@ {{ roleAssignmentCount }}
- +
@for (group of roleAssignments | keyvalue; track group.key) { From 43e0b1da147e423d7ca9a34bb4a687e881390a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Mon, 16 Jun 2025 17:51:02 +0200 Subject: [PATCH 06/16] Add button for adding role assignment --- src/Turnierplan.App/Client/src/app/i18n/de.ts | 1 + .../components/rbac-widget/rbac-widget.component.html | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Turnierplan.App/Client/src/app/i18n/de.ts b/src/Turnierplan.App/Client/src/app/i18n/de.ts index a69301f1..06a2c916 100644 --- a/src/Turnierplan.App/Client/src/app/i18n/de.ts +++ b/src/Turnierplan.App/Client/src/app/i18n/de.ts @@ -923,6 +923,7 @@ export const de = { ButtonLabel: 'Verwalten', Loading: 'Rollenzuweisungen werden geladen', NumberOfAssignments: 'Anzahl Rollenzuweisungen:', + NewRoleAssignment: 'Neue Rollenzuweisung', RoleName: { Owner: 'Besitzer', Contributor: 'Mitwirkender', diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html index f6fe5892..b0a9c1db 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html @@ -17,9 +17,11 @@ } @else { -
- +
+ {{ roleAssignmentCount }} + +
- - ({{ group.value.length }}) + +
From e395d51111f351dac428ac5b04c6ada8cd31a803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Mon, 16 Jun 2025 22:29:23 +0200 Subject: [PATCH 07/16] Some UI improvements --- src/Turnierplan.App/Client/src/app/i18n/de.ts | 2 +- .../rbac-widget/rbac-widget.component.html | 16 ++++++++++++---- .../rbac-widget/rbac-widget.component.ts | 1 + 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Turnierplan.App/Client/src/app/i18n/de.ts b/src/Turnierplan.App/Client/src/app/i18n/de.ts index 06a2c916..b559cccd 100644 --- a/src/Turnierplan.App/Client/src/app/i18n/de.ts +++ b/src/Turnierplan.App/Client/src/app/i18n/de.ts @@ -922,7 +922,6 @@ export const de = { Title: 'Zugriff verwalten', ButtonLabel: 'Verwalten', Loading: 'Rollenzuweisungen werden geladen', - NumberOfAssignments: 'Anzahl Rollenzuweisungen:', NewRoleAssignment: 'Neue Rollenzuweisung', RoleName: { Owner: 'Besitzer', @@ -938,6 +937,7 @@ export const de = { ApiKey: 'API-Schlüssel', User: 'Benutzer' }, + TotalCount: 'Gesamt:', Id: 'ID:', CreatedAt: 'Erstellt am:', NotInherited: 'Zuweisung stammt von dieser Resource', diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html index b0a9c1db..2d648c38 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html @@ -18,16 +18,17 @@ } @else {
- - {{ roleAssignmentCount }} + + {{ target.name }} +
-
+
@for (group of roleAssignments | keyvalue; track group.key) { - +
@@ -82,6 +83,13 @@ }
+ + @if (roleAssignmentCount > 1) { +
+ + {{ roleAssignmentCount }} +
+ } }
diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.ts b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.ts index eb728eda..6ae21e61 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.ts @@ -4,6 +4,7 @@ import { PrincipalKind, Role, RoleAssignmentDto, RoleAssignmentsService } from ' import { finalize } from 'rxjs'; export interface IRbacWidgetTarget { + name: string; rbacScopeId: string; } From 47ad1e029793db8661c3eba90f8d00d239b354a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Mon, 16 Jun 2025 22:30:29 +0200 Subject: [PATCH 08/16] Change style --- .../portal/components/rbac-widget/rbac-widget.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html index 2d648c38..8217150f 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html @@ -18,7 +18,7 @@ } @else {
- + {{ target.name }} From b281edcc12eaced52de46347b2eeb5bd116b2241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 21 Jun 2025 07:54:58 +0200 Subject: [PATCH 09/16] Add important TODO --- src/Turnierplan.Core/Extensions/PrincipalExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Turnierplan.Core/Extensions/PrincipalExtensions.cs b/src/Turnierplan.Core/Extensions/PrincipalExtensions.cs index 9e74bf4c..46356886 100644 --- a/src/Turnierplan.Core/Extensions/PrincipalExtensions.cs +++ b/src/Turnierplan.Core/Extensions/PrincipalExtensions.cs @@ -2,6 +2,8 @@ namespace Turnierplan.Core.Extensions; +// TODO: Add a new field "PrincipalId" for ApiKey & User | OR Make the Id of APikey also a Guid +// This is especially important for ApiKeys, so that the incrementally generated id is not "leaked" public static class PrincipalExtensions { public static Principal AsPrincipal(this ApiKey.ApiKey apiKey) From b0530f17ea827dd94a87df57d883bc25a98021c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 21 Jun 2025 07:55:23 +0200 Subject: [PATCH 10/16] Revert "Add important TODO" This reverts commit b281edcc12eaced52de46347b2eeb5bd116b2241. --- src/Turnierplan.Core/Extensions/PrincipalExtensions.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Turnierplan.Core/Extensions/PrincipalExtensions.cs b/src/Turnierplan.Core/Extensions/PrincipalExtensions.cs index 46356886..9e74bf4c 100644 --- a/src/Turnierplan.Core/Extensions/PrincipalExtensions.cs +++ b/src/Turnierplan.Core/Extensions/PrincipalExtensions.cs @@ -2,8 +2,6 @@ namespace Turnierplan.Core.Extensions; -// TODO: Add a new field "PrincipalId" for ApiKey & User | OR Make the Id of APikey also a Guid -// This is especially important for ApiKeys, so that the incrementally generated id is not "leaked" public static class PrincipalExtensions { public static Principal AsPrincipal(this ApiKey.ApiKey apiKey) From cabc56219546b68668e0ad51c45d2c7e45459eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 21 Jun 2025 08:05:14 +0200 Subject: [PATCH 11/16] Implement function to delete role assignment --- src/Turnierplan.App/Client/src/app/i18n/de.ts | 6 +++- .../rbac-widget/rbac-widget.component.ts | 32 +++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/Turnierplan.App/Client/src/app/i18n/de.ts b/src/Turnierplan.App/Client/src/app/i18n/de.ts index b559cccd..f2e6cae1 100644 --- a/src/Turnierplan.App/Client/src/app/i18n/de.ts +++ b/src/Turnierplan.App/Client/src/app/i18n/de.ts @@ -942,7 +942,11 @@ export const de = { CreatedAt: 'Erstellt am:', NotInherited: 'Zuweisung stammt von dieser Resource', Inherited: 'Vererbt durch:', - InheritedTooltip: 'Diese Rollenzuweisung existiert implizit aufgrund der Zugehörigkeit zu einer anderen Resource' + InheritedTooltip: 'Diese Rollenzuweisung existiert implizit aufgrund der Zugehörigkeit zu einer anderen Resource', + SuccessToast: { + Title: 'Rollenzuweisung gelöscht', + Message: 'Die Rollenzuweisung wurde erfolgreich gelöscht.' + } } } }; diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.ts b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.ts index 6ae21e61..1cee206b 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.ts @@ -2,6 +2,7 @@ import { Component, EventEmitter, Input, OnInit, Output, TemplateRef } from '@an import { NgbOffcanvas, NgbOffcanvasRef } from '@ng-bootstrap/ng-bootstrap'; import { PrincipalKind, Role, RoleAssignmentDto, RoleAssignmentsService } from '../../../api'; import { finalize } from 'rxjs'; +import { NotificationService } from '../../../core/services/notification.service'; export interface IRbacWidgetTarget { name: string; @@ -35,7 +36,8 @@ export class RbacWidgetComponent { constructor( private readonly offcanvasService: NgbOffcanvas, - private readonly roleAssignmentsService: RoleAssignmentsService + private readonly roleAssignmentsService: RoleAssignmentsService, + private readonly notificationService: NotificationService ) {} protected buttonClicked(template: TemplateRef): void { @@ -66,12 +68,38 @@ export class RbacWidgetComponent { }, error: (error) => { this.errorOccured.emit(error); + this.currentOffcanvas?.close(); } }); } protected removeRoleAssignment(id: string): void { - // TODO: Implement this method + this.roleAssignmentsService.deleteRoleAssignment({ scopeId: this.target.rbacScopeId, roleAssignmentId: id }).subscribe({ + next: () => { + this.roleAssignmentCount = 0; + + for (const key of Object.keys(this.roleAssignments)) { + const filtered = this.roleAssignments[key].filter((x) => x.id !== id); + this.roleAssignmentCount += filtered.length; + + if (filtered.length > 0) { + this.roleAssignments[key] = filtered; + } else { + delete this.roleAssignments[key]; + } + } + + this.notificationService.showNotification( + 'success', + 'Portal.RbacManagement.SuccessToast.Title', + 'Portal.RbacManagement.SuccessToast.Message' + ); + }, + error: (error) => { + this.errorOccured.emit(error); + this.currentOffcanvas?.close(); + } + }); } protected canDeleteAssignment(assignment: RoleAssignmentDto): boolean { From 4f9da9b2899dc6529b282053906edc97c56129dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 21 Jun 2025 08:41:05 +0200 Subject: [PATCH 12/16] Refactor offcanvas into own component + some additional features --- src/Turnierplan.App/Client/src/app/i18n/de.ts | 16 +- .../rbac-offcanvas.component.html | 86 +++++++++++ .../rbac-offcanvas.component.ts | 138 ++++++++++++++++++ .../rbac-widget/rbac-widget.component.html | 87 +---------- .../rbac-widget/rbac-widget.component.ts | 103 ++----------- .../view-organization.component.html | 1 - .../Client/src/app/portal/portal.module.ts | 4 +- .../Rules/RoleAssignmentMappingRule.cs | 3 +- .../Models/RoleAssignmentDto.cs | 4 +- .../SeedWork/IEntityWithRoleAssignments.cs | 2 + 10 files changed, 263 insertions(+), 181 deletions(-) create mode 100644 src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html create mode 100644 src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts diff --git a/src/Turnierplan.App/Client/src/app/i18n/de.ts b/src/Turnierplan.App/Client/src/app/i18n/de.ts index f2e6cae1..f9d98d21 100644 --- a/src/Turnierplan.App/Client/src/app/i18n/de.ts +++ b/src/Turnierplan.App/Client/src/app/i18n/de.ts @@ -921,6 +921,7 @@ export const de = { RbacManagement: { Title: 'Zugriff verwalten', ButtonLabel: 'Verwalten', + OffcanvasTitle: 'Rollenzuweisungen bearbeiten', Loading: 'Rollenzuweisungen werden geladen', NewRoleAssignment: 'Neue Rollenzuweisung', RoleName: { @@ -940,9 +941,22 @@ export const de = { TotalCount: 'Gesamt:', Id: 'ID:', CreatedAt: 'Erstellt am:', - NotInherited: 'Zuweisung stammt von dieser Resource', Inherited: 'Vererbt durch:', InheritedTooltip: 'Diese Rollenzuweisung existiert implizit aufgrund der Zugehörigkeit zu einer anderen Resource', + ScopeType: { + Organization: { + Info: 'Verwalten Sie, welche Nutzer auf diese Organisation zugreifen können und welche Aktionen sie durchführen können.', + NotInherited: 'Zuweisung liegt auf dieser Organisation' + }, + Tournament: { + Info: 'Verwalten Sie, welche Nutzer auf dieses Turnier zugreifen können und welche Aktionen sie durchführen können.', + NotInherited: 'Zuweisung liegt auf diesem Turnier' + }, + Venue: { + Info: 'Verwalten Sie, welche Nutzer auf diese Spielstätte zugreifen können und welche Aktionen sie durchführen können.', + NotInherited: 'Zuweisung liegt auf dieser Spielstätte' + } + }, SuccessToast: { Title: 'Rollenzuweisung gelöscht', Message: 'Die Rollenzuweisung wurde erfolgreich gelöscht.' diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html new file mode 100644 index 00000000..3ae767c7 --- /dev/null +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html @@ -0,0 +1,86 @@ +
+
+ @if (isLoadingRoleAssignments) { +
+ + +
+ } @else { +
+ + + {{ target.name }} + + + +
+ + + + @for (group of roleAssignments | keyvalue; track group.key) { + + + + + @for (assignment of group.value; track assignment.id) { + + + + } + } + +
+ + +
+
+
+ @switch (assignment.principal.kind) { + @case (PrincipalKind.ApiKey) { + + } + @case (PrincipalKind.User) { + + } + } + + {{ assignment.principal.objectId }} + @if (canDeleteAssignment(assignment)) { + + + } +
+ @if (assignment.isInherited) { + + } @else { +
+ + +
+ } +
+ + {{ assignment.id }} + · + + {{ assignment.createdAt | translateDate }} +
+
+
+ + @if (roleAssignmentCount > 1) { +
+ + {{ roleAssignmentCount }} +
+ } + } +
diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts new file mode 100644 index 00000000..2801bd3f --- /dev/null +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts @@ -0,0 +1,138 @@ +import { Component, OnDestroy } from '@angular/core'; +import { finalize, Observable, Subject } from 'rxjs'; +import { PrincipalKind, Role, RoleAssignmentDto, RoleAssignmentsService } from '../../../api'; +import { NotificationService } from '../../../core/services/notification.service'; +import { NgbActiveOffcanvas } from '@ng-bootstrap/ng-bootstrap'; + +interface IRbacOffcanvasTarget { + name: string; + rbacScopeId: string; +} + +@Component({ + standalone: false, + templateUrl: './rbac-offcanvas.component.html' +}) +export class RbacOffcanvasComponent implements OnDestroy { + protected readonly PrincipalKind = PrincipalKind; + + protected target!: IRbacOffcanvasTarget; + protected targetIcon: string = ''; + protected isLoadingRoleAssignments = false; + protected roleAssignments: { [key: string]: RoleAssignmentDto[] } = {}; + protected roleAssignmentCount: number = 0; + + protected scopeTranslationKey: string = ''; + + private readonly errorSubject$ = new Subject(); + private targetIsOrganization: boolean = false; + + constructor( + private readonly roleAssignmentsService: RoleAssignmentsService, + private readonly notificationService: NotificationService + ) {} + + public get error$(): Observable { + return this.errorSubject$.asObservable(); + } + + public ngOnDestroy(): void { + this.errorSubject$.complete(); + } + + public setTarget(target: IRbacOffcanvasTarget) { + this.target = target; + + const scopeType = target.rbacScopeId.substring(0, target.rbacScopeId.indexOf(':')); + this.targetIsOrganization = scopeType === 'Organization'; + this.scopeTranslationKey = `Portal.RbacManagement.ScopeType.${scopeType}`; + + switch (scopeType) { + case 'Organization': + this.targetIcon = 'boxes'; + break; + case 'Tournament': + this.targetIcon = 'trophy'; + break; + case 'Venue': + this.targetIcon = 'buildings'; + break; + default: + this.targetIcon = 'question-lg'; + break; + } + + this.isLoadingRoleAssignments = true; + + this.roleAssignmentsService + .getRoleAssignments({ scopeId: this.target.rbacScopeId }) + .pipe(finalize(() => (this.isLoadingRoleAssignments = false))) + .subscribe({ + next: (roleAssignments) => { + this.roleAssignments = {}; + this.roleAssignmentCount = 0; + + for (const roleAssignment of roleAssignments) { + this.roleAssignmentCount++; + + if (roleAssignment.role in this.roleAssignments) { + this.roleAssignments[roleAssignment.role].push(roleAssignment); + } else { + this.roleAssignments[roleAssignment.role] = [roleAssignment]; + } + } + }, + error: (error) => { + this.errorSubject$.next(error); + } + }); + } + + protected removeRoleAssignment(id: string): void { + this.roleAssignmentsService.deleteRoleAssignment({ scopeId: this.target.rbacScopeId, roleAssignmentId: id }).subscribe({ + next: () => { + this.roleAssignmentCount = 0; + + for (const key of Object.keys(this.roleAssignments)) { + const filtered = this.roleAssignments[key].filter((x) => x.id !== id); + this.roleAssignmentCount += filtered.length; + + if (filtered.length > 0) { + this.roleAssignments[key] = filtered; + } else { + delete this.roleAssignments[key]; + } + } + + this.notificationService.showNotification( + 'success', + 'Portal.RbacManagement.SuccessToast.Title', + 'Portal.RbacManagement.SuccessToast.Message' + ); + }, + error: (error) => { + this.errorSubject$.next(error); + } + }); + } + + protected canDeleteAssignment(assignment: RoleAssignmentDto): boolean { + if (assignment.isInherited) { + return false; + } + + if (assignment.role === Role.Owner && this.targetIsOrganization && this.roleAssignments[Role.Owner].length === 1) { + // This is a special case which forbids deleting the only Owner assignment from an organization. + return false; + } + + return true; + } + + protected navigateToScope(scopeId: string, scopeName: string): void { + this.setTarget({ + rbacScopeId: scopeId, + name: scopeName + }); + } +} diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html index 8217150f..728a9a01 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html @@ -6,90 +6,5 @@ [type]="'outline-secondary'" [icon]="'shield-lock'" [title]="'Portal.RbacManagement.ButtonLabel'" - (click)="buttonClicked(rbacManagementCanvas)" /> + (click)="buttonClicked()" />
- - -
- @if (isLoadingRoleAssignments) { -
- - -
- } @else { -
- - {{ target.name }} - - - -
- - - - @for (group of roleAssignments | keyvalue; track group.key) { - - - - - @for (assignment of group.value; track assignment.id) { - - - - } - } - -
- - -
-
-
- @switch (assignment.principal.kind) { - @case (PrincipalKind.ApiKey) { - - } - @case (PrincipalKind.User) { - - } - } - - {{ assignment.principal.objectId }} - @if (canDeleteAssignment(assignment)) { - - - } -
- @if (assignment.isInherited) { -
- - - - - {{ assignment.scope }} -
- } @else { -
- - -
- } -
- - {{ assignment.id }} - · - - {{ assignment.createdAt | translateDate }} -
-
-
- - @if (roleAssignmentCount > 1) { -
- - {{ roleAssignmentCount }} -
- } - } -
-
diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.ts b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.ts index 1cee206b..b4809e25 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.ts @@ -1,10 +1,8 @@ -import { Component, EventEmitter, Input, OnInit, Output, TemplateRef } from '@angular/core'; -import { NgbOffcanvas, NgbOffcanvasRef } from '@ng-bootstrap/ng-bootstrap'; -import { PrincipalKind, Role, RoleAssignmentDto, RoleAssignmentsService } from '../../../api'; -import { finalize } from 'rxjs'; -import { NotificationService } from '../../../core/services/notification.service'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap'; +import { RbacOffcanvasComponent } from '../rbac-offcanvas/rbac-offcanvas.component'; -export interface IRbacWidgetTarget { +interface IRbacWidgetTarget { name: string; rbacScopeId: string; } @@ -21,97 +19,22 @@ export class RbacWidgetComponent { @Input() public target!: IRbacWidgetTarget; - @Input() - public targetIsOrganization: boolean = false; - @Output() public errorOccured = new EventEmitter(); - protected readonly PrincipalKind = PrincipalKind; - - protected isLoadingRoleAssignments = false; - protected roleAssignments: { [key: string]: RoleAssignmentDto[] } = {}; - protected roleAssignmentCount: number = 0; - protected currentOffcanvas?: NgbOffcanvasRef; - - constructor( - private readonly offcanvasService: NgbOffcanvas, - private readonly roleAssignmentsService: RoleAssignmentsService, - private readonly notificationService: NotificationService - ) {} - - protected buttonClicked(template: TemplateRef): void { - this.loadRoleAssignments(); - this.currentOffcanvas = this.offcanvasService.open(template, { position: 'end' }); - } - - private loadRoleAssignments(): void { - this.isLoadingRoleAssignments = true; - - this.roleAssignmentsService - .getRoleAssignments({ scopeId: this.target.rbacScopeId }) - .pipe(finalize(() => (this.isLoadingRoleAssignments = false))) - .subscribe({ - next: (roleAssignments) => { - this.roleAssignments = {}; - this.roleAssignmentCount = 0; + constructor(private readonly offcanvasService: NgbOffcanvas) {} - for (const roleAssignment of roleAssignments) { - this.roleAssignmentCount++; + protected buttonClicked(): void { + const ref = this.offcanvasService.open(RbacOffcanvasComponent, { position: 'end' }); + const component = ref.componentInstance as RbacOffcanvasComponent; - if (roleAssignment.role in this.roleAssignments) { - this.roleAssignments[roleAssignment.role].push(roleAssignment); - } else { - this.roleAssignments[roleAssignment.role] = [roleAssignment]; - } - } - }, - error: (error) => { - this.errorOccured.emit(error); - this.currentOffcanvas?.close(); - } - }); - } - - protected removeRoleAssignment(id: string): void { - this.roleAssignmentsService.deleteRoleAssignment({ scopeId: this.target.rbacScopeId, roleAssignmentId: id }).subscribe({ - next: () => { - this.roleAssignmentCount = 0; - - for (const key of Object.keys(this.roleAssignments)) { - const filtered = this.roleAssignments[key].filter((x) => x.id !== id); - this.roleAssignmentCount += filtered.length; - - if (filtered.length > 0) { - this.roleAssignments[key] = filtered; - } else { - delete this.roleAssignments[key]; - } - } - - this.notificationService.showNotification( - 'success', - 'Portal.RbacManagement.SuccessToast.Title', - 'Portal.RbacManagement.SuccessToast.Message' - ); - }, - error: (error) => { - this.errorOccured.emit(error); - this.currentOffcanvas?.close(); + component.error$.subscribe({ + next: (value) => { + ref.close(); + this.errorOccured.emit(value); } }); - } - - protected canDeleteAssignment(assignment: RoleAssignmentDto): boolean { - if (assignment.isInherited) { - return false; - } - - if (assignment.role === Role.Owner && this.targetIsOrganization && this.roleAssignments[Role.Owner].length === 1) { - // This is a special case which forbids deleting the only Owner assignment from an organization. - return false; - } - return true; + component.setTarget(this.target); } } diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.html b/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.html index 1990ea2c..59aca8fc 100644 --- a/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.html @@ -170,7 +170,6 @@
diff --git a/src/Turnierplan.App/Client/src/app/portal/portal.module.ts b/src/Turnierplan.App/Client/src/app/portal/portal.module.ts index 9977a08d..302df2e2 100644 --- a/src/Turnierplan.App/Client/src/app/portal/portal.module.ts +++ b/src/Turnierplan.App/Client/src/app/portal/portal.module.ts @@ -80,6 +80,7 @@ import { TitleService } from './services/title.service'; import { BaseChartDirective, provideCharts, withDefaultRegisterables } from 'ng2-charts'; import { QRCodeComponent } from 'angularx-qrcode'; import { RbacWidgetComponent } from './components/rbac-widget/rbac-widget.component'; +import { RbacOffcanvasComponent } from './components/rbac-offcanvas/rbac-offcanvas.component'; const routes: Routes = [ { @@ -221,7 +222,8 @@ const routes: Routes = [ AdministrationPageComponent, CreateUserComponent, BadgeComponent, - RbacWidgetComponent + RbacWidgetComponent, + RbacOffcanvasComponent ], imports: [ CommonModule, diff --git a/src/Turnierplan.App/Mapping/Rules/RoleAssignmentMappingRule.cs b/src/Turnierplan.App/Mapping/Rules/RoleAssignmentMappingRule.cs index c491bf89..dcc03da9 100644 --- a/src/Turnierplan.App/Mapping/Rules/RoleAssignmentMappingRule.cs +++ b/src/Turnierplan.App/Mapping/Rules/RoleAssignmentMappingRule.cs @@ -19,7 +19,8 @@ protected override RoleAssignmentDto Map(IMapper mapper, MappingContext context, return new RoleAssignmentDto { Id = source.Id, - Scope = source.Scope.GetScopeId(), + ScopeId = source.Scope.GetScopeId(), + ScopeName = source.Scope.Name, CreatedAt = source.CreatedAt, Role = source.Role, Principal = new PrincipalDto diff --git a/src/Turnierplan.App/Models/RoleAssignmentDto.cs b/src/Turnierplan.App/Models/RoleAssignmentDto.cs index a1bcffe0..a39eda12 100644 --- a/src/Turnierplan.App/Models/RoleAssignmentDto.cs +++ b/src/Turnierplan.App/Models/RoleAssignmentDto.cs @@ -6,7 +6,9 @@ public sealed record RoleAssignmentDto { public required Guid Id { get; init; } - public required string Scope { get; init; } + public required string ScopeId { get; init; } + + public required string ScopeName { get; init; } public required DateTime CreatedAt { get; init; } diff --git a/src/Turnierplan.Core/SeedWork/IEntityWithRoleAssignments.cs b/src/Turnierplan.Core/SeedWork/IEntityWithRoleAssignments.cs index 50fe072e..e79a62f7 100644 --- a/src/Turnierplan.Core/SeedWork/IEntityWithRoleAssignments.cs +++ b/src/Turnierplan.Core/SeedWork/IEntityWithRoleAssignments.cs @@ -5,6 +5,8 @@ namespace Turnierplan.Core.SeedWork; public interface IEntityWithRoleAssignments : IEntityWithPublicId where T : Entity, IEntityWithRoleAssignments { + string Name { get; } + IReadOnlyList> RoleAssignments { get; } RoleAssignment AddRoleAssignment(Role role, Principal principal, string? description = null); From 5d330981074cc5742fa5dbb9c37921da4554f217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 21 Jun 2025 08:51:28 +0200 Subject: [PATCH 13/16] Add simple rbac button for folders --- .../folder-tree/folder-tree.component.ts | 2 ++ .../rbac-offcanvas.component.ts | 3 +++ .../rbac-widget/rbac-widget.component.html | 22 +++++++++++-------- .../rbac-widget/rbac-widget.component.ts | 3 +++ .../tournament-explorer.component.html | 8 ++++++- 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/Turnierplan.App/Client/src/app/portal/components/folder-tree/folder-tree.component.ts b/src/Turnierplan.App/Client/src/app/portal/components/folder-tree/folder-tree.component.ts index aa7e2320..4c7080c9 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/folder-tree/folder-tree.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/components/folder-tree/folder-tree.component.ts @@ -5,6 +5,7 @@ import { TournamentHeaderDto } from '../../../api'; export type FolderTreeEntry = { id: string; folderId?: string; + folderName?: string; label: string; isRoot: boolean; indentation: number; @@ -54,6 +55,7 @@ export class FolderTreeComponent { const folderEntry: FolderTreeEntry = { id: '/' + folder.id, folderId: folder.id, + folderName: folder.name, label: folder.name ?? '?', isRoot: false, indentation: 1, diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts index 2801bd3f..9d532b79 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts @@ -48,6 +48,9 @@ export class RbacOffcanvasComponent implements OnDestroy { this.scopeTranslationKey = `Portal.RbacManagement.ScopeType.${scopeType}`; switch (scopeType) { + case 'Folder': + this.targetIcon = 'folder2-open'; + break; case 'Organization': this.targetIcon = 'boxes'; break; diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html index 728a9a01..93545775 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html @@ -1,10 +1,14 @@ -
-
+@if (buttonOnly) { + +} @else { +
+
-
- -
+
+ +
+} diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.ts b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.ts index b4809e25..ba95cf8c 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.ts @@ -19,6 +19,9 @@ export class RbacWidgetComponent { @Input() public target!: IRbacWidgetTarget; + @Input() + public buttonOnly: boolean = false; + @Output() public errorOccured = new EventEmitter(); diff --git a/src/Turnierplan.App/Client/src/app/portal/components/tournament-explorer/tournament-explorer.component.html b/src/Turnierplan.App/Client/src/app/portal/components/tournament-explorer/tournament-explorer.component.html index e4557c61..13d4aaba 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/tournament-explorer/tournament-explorer.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/components/tournament-explorer/tournament-explorer.component.html @@ -7,7 +7,7 @@
@if (currentEntry) {
- @if (currentEntry.folderId) { + @if (currentEntry.folderId && currentEntry.folderName) {
{{ currentEntry.label }}
@if (isUpdatingFolderName) { @@ -22,7 +22,13 @@ (renamed)="renameFolder(currentEntry.folderId, $event)" /> } + + + Date: Sat, 21 Jun 2025 08:52:49 +0200 Subject: [PATCH 14/16] Fix bug when editing folder RBAC after rename --- .../tournament-explorer/tournament-explorer.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Turnierplan.App/Client/src/app/portal/components/tournament-explorer/tournament-explorer.component.ts b/src/Turnierplan.App/Client/src/app/portal/components/tournament-explorer/tournament-explorer.component.ts index c7528186..0d78c11d 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/tournament-explorer/tournament-explorer.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/components/tournament-explorer/tournament-explorer.component.ts @@ -58,6 +58,7 @@ export class TournamentExplorerComponent implements OnChanges { const folder = this.treeData.find((x) => x.folderId === folderId); if (folder) { folder.label = name; + folder.folderName = name; } this.isUpdatingFolderName = false; }, From 645979d51867863655c55f06acf959a98ec86538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 21 Jun 2025 08:55:16 +0200 Subject: [PATCH 15/16] Fix de.ts issues --- src/Turnierplan.App/Client/src/app/i18n/de.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Turnierplan.App/Client/src/app/i18n/de.ts b/src/Turnierplan.App/Client/src/app/i18n/de.ts index f9d98d21..6e9a94e0 100644 --- a/src/Turnierplan.App/Client/src/app/i18n/de.ts +++ b/src/Turnierplan.App/Client/src/app/i18n/de.ts @@ -944,16 +944,16 @@ export const de = { Inherited: 'Vererbt durch:', InheritedTooltip: 'Diese Rollenzuweisung existiert implizit aufgrund der Zugehörigkeit zu einer anderen Resource', ScopeType: { + Folder: { + NotInherited: 'Zuweisung liegt auf diesem Ordner' + }, Organization: { - Info: 'Verwalten Sie, welche Nutzer auf diese Organisation zugreifen können und welche Aktionen sie durchführen können.', NotInherited: 'Zuweisung liegt auf dieser Organisation' }, Tournament: { - Info: 'Verwalten Sie, welche Nutzer auf dieses Turnier zugreifen können und welche Aktionen sie durchführen können.', NotInherited: 'Zuweisung liegt auf diesem Turnier' }, Venue: { - Info: 'Verwalten Sie, welche Nutzer auf diese Spielstätte zugreifen können und welche Aktionen sie durchführen können.', NotInherited: 'Zuweisung liegt auf dieser Spielstätte' } }, From ea4f16fc2a283cf16cb88e525bd162154551f75a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 21 Jun 2025 08:57:24 +0200 Subject: [PATCH 16/16] rm import --- .../portal/components/rbac-offcanvas/rbac-offcanvas.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts index 9d532b79..52f12d92 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts @@ -2,7 +2,6 @@ import { Component, OnDestroy } from '@angular/core'; import { finalize, Observable, Subject } from 'rxjs'; import { PrincipalKind, Role, RoleAssignmentDto, RoleAssignmentsService } from '../../../api'; import { NotificationService } from '../../../core/services/notification.service'; -import { NgbActiveOffcanvas } from '@ng-bootstrap/ng-bootstrap'; interface IRbacOffcanvasTarget { name: string;