Enhance role selection functionality in ListSelectableRolesQueryHandler and update frontend role display
CI / Backend (build + test) (push) Successful in 1m22s
CI / Frontend (lint + typecheck + build) (push) Successful in 44s

- Updated ListSelectableRolesQueryHandler to include IIdentityService and ICurrentUser for user authorization and profile retrieval.
- Added logic to filter out the current user's role and admin roles from the selectable roles list.
- Enhanced CreateRoleRequestDialog to display role options with additional information, including max configs and IP limits, using localization support.
- Updated i18n resources to include new role option formatting for both Russian and English.
This commit is contained in:
Leonid Pershin
2026-07-14 23:31:56 +03:00
parent 8f6807a456
commit b6bffcc302
4 changed files with 114 additions and 3 deletions
@@ -1,11 +1,15 @@
using PnvPanel.Application.Auth;
using PnvPanel.Application.Common.Interfaces;
using PnvPanel.Application.Common.Messaging;
using PnvPanel.Application.Common.Models;
namespace PnvPanel.Application.Support.ListSelectableRoles;
public sealed class ListSelectableRolesQueryHandler(IRoleService roleService)
: IQueryHandler<ListSelectableRolesQuery, Result<IReadOnlyList<RoleDto>>>
public sealed class ListSelectableRolesQueryHandler(
IRoleService roleService,
IIdentityService identityService,
ICurrentUser currentUser
) : IQueryHandler<ListSelectableRolesQuery, Result<IReadOnlyList<RoleDto>>>
{
// Совпадает со значением Infrastructure.Identity.RoleNames.Admin — см. пояснение в
// CreateRoleRequestTicketCommandHandler (Application не может ссылаться на Infrastructure).
@@ -16,9 +20,16 @@ public sealed class ListSelectableRolesQueryHandler(IRoleService roleService)
CancellationToken cancellationToken
)
{
if (currentUser.UserId is not { } userId)
return Result.Failure<IReadOnlyList<RoleDto>>(AuthErrors.Unauthorized);
var profile = await identityService.GetProfileAsync(userId, cancellationToken);
var roles = await roleService.ListRolesAsync(cancellationToken);
// Без admin (нельзя запросить) и без текущей роли пользователя (уже есть — нечего запрашивать).
var selectable = roles
.Where(r => !r.Name.Equals(AdminRoleName, StringComparison.OrdinalIgnoreCase))
.Where(r => profile is null || r.Id != profile.RoleId)
.ToList();
return Result.Success<IReadOnlyList<RoleDto>>(selectable);
@@ -0,0 +1,94 @@
using NSubstitute;
using PnvPanel.Application.Common.Interfaces;
using PnvPanel.Application.Support.ListSelectableRoles;
using PnvPanel.Application.Tests.TestSupport;
using Xunit;
namespace PnvPanel.Application.Tests.Support.ListSelectableRoles;
public class ListSelectableRolesQueryHandlerTests
{
private readonly IRoleService _roleService = Substitute.For<IRoleService>();
private readonly IIdentityService _identityService = Substitute.For<IIdentityService>();
private static CurrentUserProfile Profile(Guid userId, Guid roleId, string role) =>
new(
userId,
"user",
roleId,
role,
IsActivated: true,
IsBlocked: false,
MaxConfigs: 3,
MaxIpLimit: 1,
SubscriptionToken: "token"
);
[Fact]
public async Task Handle_ExcludesAdminAndCurrentUserRole()
{
var userId = Guid.NewGuid();
var currentRoleId = Guid.NewGuid();
var extendedRoleId = Guid.NewGuid();
var adminRoleId = Guid.NewGuid();
_roleService
.ListRolesAsync(Arg.Any<CancellationToken>())
.Returns(
new List<RoleDto>
{
new(adminRoleId, "admin", -1, -1, IsSystem: true),
new(currentRoleId, "user", 3, 1, IsSystem: true),
new(extendedRoleId, "extended", 10, 5, IsSystem: false),
}
);
_identityService
.GetProfileAsync(userId, Arg.Any<CancellationToken>())
.Returns(Profile(userId, currentRoleId, "user"));
var currentUser = FakeCurrentUser.Authenticated(userId, "alice");
var handler = new ListSelectableRolesQueryHandler(
_roleService,
_identityService,
currentUser
);
var result = await handler.Handle(new ListSelectableRolesQuery(), CancellationToken.None);
Assert.True(result.IsSuccess);
Assert.Equal(["extended"], result.Value.Select(r => r.Name));
}
[Fact]
public async Task Handle_WhenProfileMissing_StillExcludesAdmin()
{
var userId = Guid.NewGuid();
var userRoleId = Guid.NewGuid();
var adminRoleId = Guid.NewGuid();
_roleService
.ListRolesAsync(Arg.Any<CancellationToken>())
.Returns(
new List<RoleDto>
{
new(adminRoleId, "admin", -1, -1, IsSystem: true),
new(userRoleId, "user", 3, 1, IsSystem: true),
}
);
_identityService
.GetProfileAsync(userId, Arg.Any<CancellationToken>())
.Returns((CurrentUserProfile?)null);
var currentUser = FakeCurrentUser.Authenticated(userId, "alice");
var handler = new ListSelectableRolesQueryHandler(
_roleService,
_identityService,
currentUser
);
var result = await handler.Handle(new ListSelectableRolesQuery(), CancellationToken.None);
Assert.True(result.IsSuccess);
Assert.Equal(["user"], result.Value.Select(r => r.Name));
}
}
@@ -107,7 +107,11 @@ export function CreateRoleRequestDialog() {
<SelectContent>
{rolesQuery.data?.map((role) => (
<SelectItem key={role.id} value={role.id}>
{role.name}
{t('support.roleOption', {
name: role.name,
configs: role.maxConfigs < 0 ? t('unlimited') : role.maxConfigs,
ip: role.maxIpLimit < 0 ? t('unlimited') : role.maxIpLimit,
})}
</SelectItem>
))}
</SelectContent>
+2
View File
@@ -131,6 +131,7 @@ const resources = {
existingRole: 'Существующая роль',
newRole: 'Новая роль',
selectRole: 'Выберите роль',
roleOption: '{{name}} — конфигов: {{configs}}, IP: {{ip}}',
newRoleName: 'Название роли',
newRoleMaxConfigs: 'Количество конфигов (-1 — без лимита)',
newRoleMaxIpLimit: 'Количество IP (-1 — без лимита)',
@@ -548,6 +549,7 @@ const resources = {
existingRole: 'Existing role',
newRole: 'New role',
selectRole: 'Select a role',
roleOption: '{{name}} — configs: {{configs}}, IP: {{ip}}',
newRoleName: 'Role name',
newRoleMaxConfigs: 'Max configs (-1 = unlimited)',
newRoleMaxIpLimit: 'Max IPs (-1 = unlimited)',