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
index 758ef58d..c1a85dfd 100644
--- 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
@@ -41,7 +41,7 @@
}
@if (assignment.isInherited) {
-
+
}
}
-
-{{ principal.principalId }}
+{{ displayName$ | async }}
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
index 63a450ba..4cdba553 100644
--- 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
@@ -1,14 +1,33 @@
-import { Component, Input } from '@angular/core';
+import { Component, Input, OnInit } from '@angular/core';
import { PrincipalDto, PrincipalKind } from '../../../api';
+import { PrincipalsService } from '../../../api/services/principals.service';
+import { catchError, Observable, of } from 'rxjs';
+import { map } from 'rxjs/operators';
@Component({
selector: 'tp-rbac-principal',
standalone: false,
templateUrl: './rbac-principal.component.html'
})
-export class RbacPrincipalComponent {
+export class RbacPrincipalComponent implements OnInit {
protected readonly PrincipalKind = PrincipalKind;
+ protected displayName$?: Observable;
+
@Input()
public principal!: PrincipalDto;
+
+ constructor(private readonly principalsService: PrincipalsService) {}
+
+ public ngOnInit(): void {
+ this.displayName$ = this.principalsService
+ .getPrincipalName({
+ principalKind: this.principal.kind,
+ principalId: this.principal.principalId
+ })
+ .pipe(
+ map((result) => result.name),
+ catchError(() => of(this.principal.principalId))
+ );
+ }
}
diff --git a/src/Turnierplan.App/Endpoints/Principals/GetPrincipalNameEndpoint.cs b/src/Turnierplan.App/Endpoints/Principals/GetPrincipalNameEndpoint.cs
new file mode 100644
index 00000000..256d0a62
--- /dev/null
+++ b/src/Turnierplan.App/Endpoints/Principals/GetPrincipalNameEndpoint.cs
@@ -0,0 +1,44 @@
+using Microsoft.AspNetCore.Mvc;
+using Turnierplan.Core.ApiKey;
+using Turnierplan.Core.RoleAssignment;
+using Turnierplan.Core.User;
+
+namespace Turnierplan.App.Endpoints.Principals;
+
+internal sealed class GetPrincipalNameEndpoint : EndpointBase
+{
+ protected override HttpMethod Method => HttpMethod.Get;
+
+ protected override string Route => "/api/principals/name";
+
+ protected override Delegate Handler => Handle;
+
+ private static async Task Handle(
+ [FromQuery] PrincipalKind principalKind,
+ [FromQuery] Guid principalId,
+ IApiKeyRepository apiKeyRepository,
+ IUserRepository userRepository)
+ {
+ string? name;
+
+ switch (principalKind)
+ {
+ case PrincipalKind.ApiKey:
+ var apiKey = await apiKeyRepository.GetByPrincipalIdAsync(principalId).ConfigureAwait(false);
+ name = apiKey?.Name;
+ break;
+ case PrincipalKind.User:
+ var user = await userRepository.GetByPrincipalIdAsync(principalId).ConfigureAwait(false);
+ name = user?.Name;
+ break;
+ default:
+ return Results.BadRequest("Invalid principal kind specified.");
+ }
+
+ return name is not null
+ ? Results.Ok(new GetPrincipalNameEndpointResponse(name))
+ : Results.NotFound();
+ }
+
+ public sealed record GetPrincipalNameEndpointResponse(string Name);
+}
diff --git a/src/Turnierplan.Core/ApiKey/IApiKeyRepository.cs b/src/Turnierplan.Core/ApiKey/IApiKeyRepository.cs
index 6bd5c7d5..b468ece4 100644
--- a/src/Turnierplan.Core/ApiKey/IApiKeyRepository.cs
+++ b/src/Turnierplan.Core/ApiKey/IApiKeyRepository.cs
@@ -4,5 +4,7 @@ namespace Turnierplan.Core.ApiKey;
public interface IApiKeyRepository : IRepositoryWithPublicId
{
+ Task GetByPrincipalIdAsync(Guid id);
+
Task> GetRequestsInTimeRange(ApiKey apiKey, DateTime start, DateTime end);
}
diff --git a/src/Turnierplan.Core/User/IUserRepository.cs b/src/Turnierplan.Core/User/IUserRepository.cs
index 7b94f56a..e3703563 100644
--- a/src/Turnierplan.Core/User/IUserRepository.cs
+++ b/src/Turnierplan.Core/User/IUserRepository.cs
@@ -8,5 +8,7 @@ public interface IUserRepository : IRepository
Task GetByIdAsync(Guid id);
+ Task GetByPrincipalIdAsync(Guid id);
+
Task GetByEmailAsync(string email);
}
diff --git a/src/Turnierplan.Dal/Repositories/ApiKeyRepository.cs b/src/Turnierplan.Dal/Repositories/ApiKeyRepository.cs
index 608f65d9..ff3d68c1 100644
--- a/src/Turnierplan.Dal/Repositories/ApiKeyRepository.cs
+++ b/src/Turnierplan.Dal/Repositories/ApiKeyRepository.cs
@@ -23,6 +23,11 @@ public ApiKeyRepository(TurnierplanContext context)
.FirstOrDefaultAsync();
}
+ public Task GetByPrincipalIdAsync(Guid id)
+ {
+ return DbSet.Where(x => x.PrincipalId == id).FirstOrDefaultAsync();
+ }
+
public Task> GetRequestsInTimeRange(ApiKey apiKey, DateTime start, DateTime end)
{
return _requests.Where(x => x.ApiKey == apiKey && x.Timestamp >= start && x.Timestamp <= end)
diff --git a/src/Turnierplan.Dal/Repositories/UserRepository.cs b/src/Turnierplan.Dal/Repositories/UserRepository.cs
index 3fb65403..d4b4deac 100644
--- a/src/Turnierplan.Dal/Repositories/UserRepository.cs
+++ b/src/Turnierplan.Dal/Repositories/UserRepository.cs
@@ -16,6 +16,11 @@ public Task> GetAllUsers()
return DbSet.Where(x => x.Id.Equals(id)).FirstOrDefaultAsync();
}
+ public Task GetByPrincipalIdAsync(Guid id)
+ {
+ return DbSet.Where(x => x.PrincipalId == id).FirstOrDefaultAsync();
+ }
+
public Task GetByEmailAsync(string email)
{
var normalizedEMail = User.NormalizeEmail(email);