- @switch (assignment.principal.kind) {
- @case (PrincipalKind.ApiKey) {
-
- }
- @case (PrincipalKind.User) {
-
- }
- }
-
- {{ assignment.principal.principalId }}
+
@if (canDeleteAssignment(assignment)) {
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 52f12d92..2fcf28a4 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
@@ -1,7 +1,9 @@
import { Component, OnDestroy } from '@angular/core';
import { finalize, Observable, Subject } from 'rxjs';
-import { PrincipalKind, Role, RoleAssignmentDto, RoleAssignmentsService } from '../../../api';
+import { Role, RoleAssignmentDto, RoleAssignmentsService } from '../../../api';
import { NotificationService } from '../../../core/services/notification.service';
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
+import { RbacAddAssignmentComponent } from '../rbac-add-assignment/rbac-add-assignment.component';
interface IRbacOffcanvasTarget {
name: string;
@@ -13,8 +15,6 @@ interface IRbacOffcanvasTarget {
templateUrl: './rbac-offcanvas.component.html'
})
export class RbacOffcanvasComponent implements OnDestroy {
- protected readonly PrincipalKind = PrincipalKind;
-
protected target!: IRbacOffcanvasTarget;
protected targetIcon: string = '';
protected isLoadingRoleAssignments = false;
@@ -28,7 +28,8 @@ export class RbacOffcanvasComponent implements OnDestroy {
constructor(
private readonly roleAssignmentsService: RoleAssignmentsService,
- private readonly notificationService: NotificationService
+ private readonly notificationService: NotificationService,
+ private readonly modalService: NgbModal
) {}
public get error$(): Observable {
@@ -64,30 +65,7 @@ export class RbacOffcanvasComponent implements OnDestroy {
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);
- }
- });
+ this.loadRoleAssignments();
}
protected removeRoleAssignment(id: string): void {
@@ -108,8 +86,8 @@ export class RbacOffcanvasComponent implements OnDestroy {
this.notificationService.showNotification(
'success',
- 'Portal.RbacManagement.SuccessToast.Title',
- 'Portal.RbacManagement.SuccessToast.Message'
+ 'Portal.RbacManagement.DeletedSuccessToast.Title',
+ 'Portal.RbacManagement.DeletedSuccessToast.Message'
);
},
error: (error) => {
@@ -137,4 +115,46 @@ export class RbacOffcanvasComponent implements OnDestroy {
name: scopeName
});
}
+
+ protected showAddRoleAssignmentDialog(): void {
+ const ref = this.modalService.open(RbacAddAssignmentComponent, {
+ size: 'lg',
+ fullscreen: 'lg',
+ centered: true
+ });
+
+ const component = ref.componentInstance as RbacAddAssignmentComponent;
+
+ component.scopeId = this.target.rbacScopeId;
+ component.error$.subscribe((error) => this.errorSubject$.next(error));
+
+ ref.closed.subscribe(() => this.loadRoleAssignments());
+ }
+
+ 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.errorSubject$.next(error);
+ }
+ });
+ }
}
diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-principal/rbac-principal.component.html b/src/Turnierplan.App/Client/src/app/portal/components/rbac-principal/rbac-principal.component.html
new file mode 100644
index 00000000..d3d7f345
--- /dev/null
+++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-principal/rbac-principal.component.html
@@ -0,0 +1,10 @@
+@switch (principal.kind) {
+ @case (PrincipalKind.ApiKey) {
+
+ }
+ @case (PrincipalKind.User) {
+
+ }
+}
+
+{{ principal.principalId }}
diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-principal/rbac-principal.component.ts b/src/Turnierplan.App/Client/src/app/portal/components/rbac-principal/rbac-principal.component.ts
new file mode 100644
index 00000000..63a450ba
--- /dev/null
+++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-principal/rbac-principal.component.ts
@@ -0,0 +1,14 @@
+import { Component, Input } from '@angular/core';
+import { PrincipalDto, PrincipalKind } from '../../../api';
+
+@Component({
+ selector: 'tp-rbac-principal',
+ standalone: false,
+ templateUrl: './rbac-principal.component.html'
+})
+export class RbacPrincipalComponent {
+ protected readonly PrincipalKind = PrincipalKind;
+
+ @Input()
+ public principal!: PrincipalDto;
+}
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 ba95cf8c..e86989b2 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,6 +1,9 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
-import { NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap';
+import { NgbOffcanvas, NgbOffcanvasRef } from '@ng-bootstrap/ng-bootstrap';
import { RbacOffcanvasComponent } from '../rbac-offcanvas/rbac-offcanvas.component';
+import { NavigationStart, Router } from '@angular/router';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { filter } from 'rxjs/operators';
interface IRbacWidgetTarget {
name: string;
@@ -25,15 +28,31 @@ export class RbacWidgetComponent {
@Output()
public errorOccured = new EventEmitter();
- constructor(private readonly offcanvasService: NgbOffcanvas) {}
+ private offcanvas?: NgbOffcanvasRef;
+
+ constructor(
+ private readonly offcanvasService: NgbOffcanvas,
+ private readonly router: Router
+ ) {
+ this.router.events
+ .pipe(
+ takeUntilDestroyed(),
+ filter((event) => event instanceof NavigationStart)
+ )
+ .subscribe({
+ next: () => {
+ this.offcanvas?.close();
+ }
+ });
+ }
protected buttonClicked(): void {
- const ref = this.offcanvasService.open(RbacOffcanvasComponent, { position: 'end' });
- const component = ref.componentInstance as RbacOffcanvasComponent;
+ this.offcanvas = this.offcanvasService.open(RbacOffcanvasComponent, { position: 'end' });
+ const component = this.offcanvas.componentInstance as RbacOffcanvasComponent;
component.error$.subscribe({
next: (value) => {
- ref.close();
+ this.offcanvas?.close();
this.errorOccured.emit(value);
}
});
diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/administration-page/administration-page.component.ts b/src/Turnierplan.App/Client/src/app/portal/pages/administration-page/administration-page.component.ts
index ecbda077..de138961 100644
--- a/src/Turnierplan.App/Client/src/app/portal/pages/administration-page/administration-page.component.ts
+++ b/src/Turnierplan.App/Client/src/app/portal/pages/administration-page/administration-page.component.ts
@@ -9,6 +9,8 @@ import { NotificationService } from '../../../core/services/notification.service
import { PageFrameNavigationTab } from '../../components/page-frame/page-frame.component';
import { LoadingState } from '../../directives/loading-state/loading-state.directive';
import { TitleService } from '../../services/title.service';
+import { NavigationStart, Router } from '@angular/router';
+import { filter } from 'rxjs/operators';
@Component({
standalone: false,
@@ -35,9 +37,21 @@ export class AdministrationPageComponent implements OnInit {
private readonly titleService: TitleService,
private readonly authenticationService: AuthenticationService,
private readonly offcanvasService: NgbOffcanvas,
- private readonly notificationService: NotificationService
+ private readonly notificationService: NotificationService,
+ private readonly router: Router
) {
this.authenticationService.authentication$.pipe(takeUntilDestroyed()).subscribe((userInfo) => (this.currentUserId = userInfo.id));
+
+ this.router.events
+ .pipe(
+ takeUntilDestroyed(),
+ filter((event) => event instanceof NavigationStart)
+ )
+ .subscribe({
+ next: () => {
+ this.currentOffcanvas?.close();
+ }
+ });
}
public ngOnInit(): void {
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 302df2e2..183ef7b4 100644
--- a/src/Turnierplan.App/Client/src/app/portal/portal.module.ts
+++ b/src/Turnierplan.App/Client/src/app/portal/portal.module.ts
@@ -81,6 +81,8 @@ import { BaseChartDirective, provideCharts, withDefaultRegisterables } from 'ng2
import { QRCodeComponent } from 'angularx-qrcode';
import { RbacWidgetComponent } from './components/rbac-widget/rbac-widget.component';
import { RbacOffcanvasComponent } from './components/rbac-offcanvas/rbac-offcanvas.component';
+import { RbacPrincipalComponent } from './components/rbac-principal/rbac-principal.component';
+import { RbacAddAssignmentComponent } from './components/rbac-add-assignment/rbac-add-assignment.component';
const routes: Routes = [
{
@@ -223,7 +225,9 @@ const routes: Routes = [
CreateUserComponent,
BadgeComponent,
RbacWidgetComponent,
- RbacOffcanvasComponent
+ RbacOffcanvasComponent,
+ RbacAddAssignmentComponent,
+ RbacPrincipalComponent
],
imports: [
CommonModule,
diff --git a/src/Turnierplan.App/Endpoints/RoleAssignments/CreateRoleAssignmentEndpoint.cs b/src/Turnierplan.App/Endpoints/RoleAssignments/CreateRoleAssignmentEndpoint.cs
index ddd14de3..82acdbcf 100644
--- a/src/Turnierplan.App/Endpoints/RoleAssignments/CreateRoleAssignmentEndpoint.cs
+++ b/src/Turnierplan.App/Endpoints/RoleAssignments/CreateRoleAssignmentEndpoint.cs
@@ -144,7 +144,7 @@ public sealed record CreateRoleAssignmentEndpointRequest
public required string? UserEmail { get; init; }
- public required string Description { get; init; }
+ public required string? Description { get; init; }
}
private sealed class Validator : AbstractValidator
@@ -164,7 +164,9 @@ private Validator()
.WithMessage($"Exactly only one of {nameof(CreateRoleAssignmentEndpointRequest.ApiKeyId)} and {nameof(CreateRoleAssignmentEndpointRequest.UserEmail)} must be specified.");
RuleFor(x => x.Description)
- .MaximumLength(ValidationConstants.RoleAssignment.MaxDescriptionLength);
+ .NotEmpty()
+ .MaximumLength(ValidationConstants.RoleAssignment.MaxDescriptionLength)
+ .When(x => x.Description is not null);
}
}
}
diff --git a/src/Turnierplan.App/Helpers/DeletionHelper.cs b/src/Turnierplan.App/Helpers/DeletionHelper.cs
index 66bef579..d1c1b1b4 100644
--- a/src/Turnierplan.App/Helpers/DeletionHelper.cs
+++ b/src/Turnierplan.App/Helpers/DeletionHelper.cs
@@ -45,6 +45,7 @@ public DeletionHelper(
public async Task DeleteUserAsync(User user, CancellationToken cancellationToken)
{
// TODO: Decide how to handle this (there is no longer a 1-n relation between user and organisation)
+ // When this is decided: Update the information text on the "delete user" screen in the frontend
// foreach (var organization in user.Organizations.ToList()) // ToList() to avoid invalid operation exception
// {