From b6bffcc302ee74877d81644013bf5867bbe16844 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Tue, 14 Jul 2026 23:31:56 +0300 Subject: [PATCH] Enhance role selection functionality in ListSelectableRolesQueryHandler and update frontend role display - 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. --- .../ListSelectableRolesQueryHandler.cs | 15 ++- .../ListSelectableRolesQueryHandlerTests.cs | 94 +++++++++++++++++++ .../support/CreateRoleRequestDialog.tsx | 6 +- frontend/src/shared/lib/i18n.ts | 2 + 4 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 backend/tests/PnvPanel.Application.Tests/Support/ListSelectableRoles/ListSelectableRolesQueryHandlerTests.cs diff --git a/backend/src/PnvPanel.Application/Support/ListSelectableRoles/ListSelectableRolesQueryHandler.cs b/backend/src/PnvPanel.Application/Support/ListSelectableRoles/ListSelectableRolesQueryHandler.cs index 09f3850..e910896 100644 --- a/backend/src/PnvPanel.Application/Support/ListSelectableRoles/ListSelectableRolesQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Support/ListSelectableRoles/ListSelectableRolesQueryHandler.cs @@ -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>> +public sealed class ListSelectableRolesQueryHandler( + IRoleService roleService, + IIdentityService identityService, + ICurrentUser currentUser +) : IQueryHandler>> { // Совпадает со значением 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>(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>(selectable); diff --git a/backend/tests/PnvPanel.Application.Tests/Support/ListSelectableRoles/ListSelectableRolesQueryHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Support/ListSelectableRoles/ListSelectableRolesQueryHandlerTests.cs new file mode 100644 index 0000000..bc05659 --- /dev/null +++ b/backend/tests/PnvPanel.Application.Tests/Support/ListSelectableRoles/ListSelectableRolesQueryHandlerTests.cs @@ -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(); + private readonly IIdentityService _identityService = Substitute.For(); + + 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()) + .Returns( + new List + { + 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()) + .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()) + .Returns( + new List + { + new(adminRoleId, "admin", -1, -1, IsSystem: true), + new(userRoleId, "user", 3, 1, IsSystem: true), + } + ); + _identityService + .GetProfileAsync(userId, Arg.Any()) + .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)); + } +} diff --git a/frontend/src/features/support/CreateRoleRequestDialog.tsx b/frontend/src/features/support/CreateRoleRequestDialog.tsx index 1bbad41..fb190e9 100644 --- a/frontend/src/features/support/CreateRoleRequestDialog.tsx +++ b/frontend/src/features/support/CreateRoleRequestDialog.tsx @@ -107,7 +107,11 @@ export function CreateRoleRequestDialog() { {rolesQuery.data?.map((role) => ( - {role.name} + {t('support.roleOption', { + name: role.name, + configs: role.maxConfigs < 0 ? t('unlimited') : role.maxConfigs, + ip: role.maxIpLimit < 0 ? t('unlimited') : role.maxIpLimit, + })} ))} diff --git a/frontend/src/shared/lib/i18n.ts b/frontend/src/shared/lib/i18n.ts index f1d921e..f236b13 100644 --- a/frontend/src/shared/lib/i18n.ts +++ b/frontend/src/shared/lib/i18n.ts @@ -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)',