From 98add9e39828c3e576de69a83a2e2288c1c6a8d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:19:57 +0300 Subject: [PATCH] Permission Granted Users --- .../Identity/Dto/PermissionGrantedUsersDto.cs | 28 +++ .../Identity/PlatformIdentityAppService.cs | 118 ++++++++++ .../Seeds/LanguagesData.json | 18 ++ ui/src/components/ui/Tag/Tag.tsx | 4 +- ui/src/services/identity.service.ts | 29 +++ ui/src/views/list/GridFilterDialogs.tsx | 14 ++ ui/src/views/list/PermissionGranted.tsx | 216 ++++++++++++++++++ ui/src/views/list/useFilters.tsx | 26 +++ 8 files changed, 452 insertions(+), 1 deletion(-) create mode 100644 api/src/Sozsoft.Platform.Application.Contracts/Identity/Dto/PermissionGrantedUsersDto.cs create mode 100644 ui/src/views/list/PermissionGranted.tsx diff --git a/api/src/Sozsoft.Platform.Application.Contracts/Identity/Dto/PermissionGrantedUsersDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/Identity/Dto/PermissionGrantedUsersDto.cs new file mode 100644 index 0000000..1a912b0 --- /dev/null +++ b/api/src/Sozsoft.Platform.Application.Contracts/Identity/Dto/PermissionGrantedUsersDto.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + +namespace Sozsoft.Platform.Identity.Dto; + +public class PermissionGrantedUsersDto +{ + public string Name { get; set; } + public string ParentName { get; set; } + public string DisplayName { get; set; } + public bool IsEnabled { get; set; } + public List RoleNames { get; set; } = []; + public List Users { get; set; } = []; +} + +public class PermissionGrantedUserDto +{ + public Guid? TenantId { get; set; } + public Guid Id { get; set; } + public string UserName { get; set; } + public string Name { get; set; } + public string Surname { get; set; } + public string FullName { get; set; } + public string Email { get; set; } + public bool IsActive { get; set; } + public bool IsDirectGrant { get; set; } + public List RoleNames { get; set; } = []; +} diff --git a/api/src/Sozsoft.Platform.Application/Identity/PlatformIdentityAppService.cs b/api/src/Sozsoft.Platform.Application/Identity/PlatformIdentityAppService.cs index c644ab8..0eb9a3e 100644 --- a/api/src/Sozsoft.Platform.Application/Identity/PlatformIdentityAppService.cs +++ b/api/src/Sozsoft.Platform.Application/Identity/PlatformIdentityAppService.cs @@ -23,8 +23,10 @@ public class PlatformIdentityAppService : ApplicationService { public IIdentityUserAppService IdentityUserAppService { get; } private readonly IIdentityUserRepository identityUserRepository; + private readonly IIdentityRoleRepository identityRoleRepository; private readonly IIdentitySessionRepository identitySessionRepository; private readonly IOpenIddictTokenManager openIddictTokenManager; + private readonly IPermissionGrantRepository permissionGrantRepository; public IRepository permissionRepository { get; } public IRepository branchRepository { get; } public IRepository branchUsersRepository { get; } @@ -39,8 +41,10 @@ public class PlatformIdentityAppService : ApplicationService public PlatformIdentityAppService( IIdentityUserAppService identityUserAppService, IIdentityUserRepository identityUserRepository, + IIdentityRoleRepository identityRoleRepository, IIdentitySessionRepository identitySessionRepository, IOpenIddictTokenManager openIddictTokenManager, + IPermissionGrantRepository permissionGrantRepository, IRepository permissionRepository, IRepository branchRepository, IRepository branchUsersRepository, @@ -54,8 +58,10 @@ public class PlatformIdentityAppService : ApplicationService { this.IdentityUserAppService = identityUserAppService; this.identityUserRepository = identityUserRepository; + this.identityRoleRepository = identityRoleRepository; this.identitySessionRepository = identitySessionRepository; this.openIddictTokenManager = openIddictTokenManager; + this.permissionGrantRepository = permissionGrantRepository; this.workHourRepository = workHourRepository; this.departmentRepository = departmentRepository; this.jobPositionRepository = jobPositionRepository; @@ -301,6 +307,118 @@ public class PlatformIdentityAppService : ApplicationService return [.. list.OrderBy(p => p.Name)]; } + public async Task> GetPermissionGrantedUsersAsync(string permissionName) + { + if (string.IsNullOrWhiteSpace(permissionName)) + { + return []; + } + + permissionName = permissionName.Trim(); + + var permissions = (await permissionRepository.GetListAsync()) + .Where(p => p.Name == permissionName || p.ParentName == permissionName) + .OrderBy(p => p.ParentName == null ? 0 : 1) + .ThenBy(p => p.Name) + .Take(200) + .ToList(); + + var permissionNames = permissions.Select(p => p.Name).ToHashSet(); + if (permissionNames.Count == 0) + { + return []; + } + + var grants = (await permissionGrantRepository.GetListAsync()) + .Where(g => permissionNames.Contains(g.Name) && (g.ProviderName == "R" || g.ProviderName == "U")) + .ToList(); + + var roleNames = grants + .Where(g => g.ProviderName == "R") + .Select(g => g.ProviderKey) + .Where(key => !string.IsNullOrWhiteSpace(key)) + .Distinct() + .ToList(); + + var roles = roleNames.Count == 0 + ? [] + : (await identityRoleRepository.GetListAsync()) + .Where(r => roleNames.Contains(r.Name)) + .ToList(); + + var roleIdByName = roles.ToDictionary(r => r.Name, r => r.Id); + var roleNameById = roles.ToDictionary(r => r.Id, r => r.Name); + + var roleIds = roleIdByName.Values.ToHashSet(); + var directUserIds = grants + .Where(g => g.ProviderName == "U" && Guid.TryParse(g.ProviderKey, out _)) + .Select(g => Guid.Parse(g.ProviderKey)) + .ToHashSet(); + + var users = (await identityUserRepository.GetListAsync(includeDetails: true)) + .Where(user => + directUserIds.Contains(user.Id) || + (user.Roles?.Any(userRole => roleIds.Contains(userRole.RoleId)) ?? false)) + .ToList(); + + return permissions.Select(permission => + { + var permissionGrants = grants.Where(g => g.Name == permission.Name).ToList(); + var permissionRoleNames = permissionGrants + .Where(g => g.ProviderName == "R") + .Select(g => g.ProviderKey) + .Where(roleName => !string.IsNullOrWhiteSpace(roleName) && roleIdByName.ContainsKey(roleName)) + .Distinct() + .OrderBy(roleName => roleName) + .ToList(); + var permissionRoleIds = permissionRoleNames.Select(roleName => roleIdByName[roleName]).ToHashSet(); + var permissionDirectUserIds = permissionGrants + .Where(g => g.ProviderName == "U" && Guid.TryParse(g.ProviderKey, out _)) + .Select(g => Guid.Parse(g.ProviderKey)) + .ToHashSet(); + + var permissionUsers = users + .Where(user => + permissionDirectUserIds.Contains(user.Id) || + (user.Roles?.Any(userRole => permissionRoleIds.Contains(userRole.RoleId)) ?? false)) + .Select(user => + { + var userRoleNames = (user.Roles ?? []) + .Where(userRole => permissionRoleIds.Contains(userRole.RoleId)) + .Select(userRole => roleNameById[userRole.RoleId]) + .Distinct() + .OrderBy(roleName => roleName) + .ToList(); + + return new PermissionGrantedUserDto + { + TenantId = user.TenantId, + Id = user.Id, + UserName = user.UserName, + Name = user.Name, + Surname = user.Surname, + FullName = $"{user.Name} {user.Surname}".Trim(), + Email = user.Email, + IsActive = user.IsActive, + IsDirectGrant = permissionDirectUserIds.Contains(user.Id), + RoleNames = userRoleNames + }; + }) + .OrderBy(user => user.UserName) + .ToList(); + + return new PermissionGrantedUsersDto + { + Name = permission.Name, + ParentName = permission.ParentName, + DisplayName = permission.DisplayName, + IsEnabled = permission.IsEnabled, + RoleNames = permissionRoleNames, + Users = permissionUsers + }; + }).ToList(); + } + public async Task CreateClaimUserAsync(UserClaimModel input) { var user = await identityUserRepository.GetAsync(input.UserId); diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json index c11576a..7a29a3c 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json @@ -4236,6 +4236,24 @@ "en": "Reset list form structure", "tr": "Liste form yapısını sıfırla" }, + { + "resourceName": "Platform", + "key": "ListForms.ListForm.GrantedUsersErrorLoading", + "en": "Authorized users could not be loaded.", + "tr": "Yetki kullanıcıları yüklenemedi" + }, + { + "resourceName": "Platform", + "key": "ListForms.ListForm.GrantedUsersNoAuthorization", + "en": "No authorization record was found for this list.", + "tr": "Bu liste için yetki kaydı bulunamadı." + }, + { + "resourceName": "Platform", + "key": "ListForms.ListForm.GrantedUsers", + "en": "Authorized users", + "tr": "Yetkili kullanıcılar" + }, { "resourceName": "Platform", "key": "ListForms.ListForm.Manage", diff --git a/ui/src/components/ui/Tag/Tag.tsx b/ui/src/components/ui/Tag/Tag.tsx index 3dfb39e..18c4f41 100644 --- a/ui/src/components/ui/Tag/Tag.tsx +++ b/ui/src/components/ui/Tag/Tag.tsx @@ -9,6 +9,7 @@ export interface TagProps extends CommonProps { prefixClass?: string suffix?: boolean | ReactNode suffixClass?: string + title?: string } const Tag = forwardRef((props, ref) => { @@ -19,11 +20,12 @@ const Tag = forwardRef((props, ref) => { suffix, prefixClass, suffixClass, + title, ...rest } = props return ( -
+
{prefix && typeof prefix === 'boolean' && ( apiService.fetchData>({ method: 'GET', @@ -84,6 +106,13 @@ export const getPermissionsList = () => url: `/api/app/platform-identity/permission-list`, }) +export const getPermissionGrantedUsers = (permissionName: string) => + apiService.fetchData({ + method: 'GET', + url: '/api/app/platform-identity/permission-granted-users', + params: { permissionName }, + }) + export const updatePermissions = ( providerName: string, providerKey: string, diff --git a/ui/src/views/list/GridFilterDialogs.tsx b/ui/src/views/list/GridFilterDialogs.tsx index 5c5bc20..2ed59bd 100644 --- a/ui/src/views/list/GridFilterDialogs.tsx +++ b/ui/src/views/list/GridFilterDialogs.tsx @@ -8,6 +8,7 @@ import { Dispatch, MutableRefObject, SetStateAction, useState } from 'react' import CreatableSelect from 'react-select/creatable' import { ISelectBoxData } from './useFilters' import { ListFormCustomizationTypeEnum } from '@/proxy/form/models' +import PermissionGranted from './PermissionGranted' const GridFilterDialogs = (props: { listFormCode: string @@ -17,6 +18,9 @@ const GridFilterDialogs = (props: { setIsCreateUpdateModalOpen: Dispatch> isDeleteModalOpen: boolean setIsDeleteModalOpen: Dispatch> + isPermissionUsersModalOpen: boolean + setIsPermissionUsersModalOpen: Dispatch> + permissionNames: string[] getFilters: () => Promise }) => { const [newFilterName, setNewFilterName] = useState() @@ -30,6 +34,9 @@ const GridFilterDialogs = (props: { setIsCreateUpdateModalOpen, isDeleteModalOpen, setIsDeleteModalOpen, + isPermissionUsersModalOpen, + setIsPermissionUsersModalOpen, + permissionNames, getFilters, } = props @@ -164,6 +171,13 @@ const GridFilterDialogs = (props: {
+ + setIsPermissionUsersModalOpen(false)} + /> ) } diff --git a/ui/src/views/list/PermissionGranted.tsx b/ui/src/views/list/PermissionGranted.tsx new file mode 100644 index 0000000..ee39d78 --- /dev/null +++ b/ui/src/views/list/PermissionGranted.tsx @@ -0,0 +1,216 @@ +import { Avatar, Button, Dialog, Notification, Spinner, Table, Tag, toast } from '@/components/ui' +import { AVATAR_URL } from '@/constants/app.constant' +import { + getPermissionGrantedUsers, + PermissionGrantedUsersDto, + PermissionGrantedUserDto, +} from '@/services/identity.service' +import { useLocalization } from '@/utils/hooks/useLocalization' +import { useEffect, useMemo, useState } from 'react' + +type PermissionGrantedProps = { + isOpen: boolean + listFormCode: string + permissionNames: string[] + onClose: () => void +} + +const permissionActionSuffixes = ['Create', 'Delete', 'Export', 'Import', 'Note', 'Update'] + +const actionLabels: Record = { + Create: 'Create', + Delete: 'Delete', + Export: 'Export', + Import: 'Import', + Note: 'Note', + Update: 'Update', +} + +const permissionActionLabel = (permissionName: string) => { + const lastPart = permissionName.split('.').pop() ?? permissionName + return actionLabels[lastPart] ?? lastPart +} + +const unique = (values: string[]) => + values.filter((value, index, array) => value && array.indexOf(value) === index) + +const toRootPermissionName = (permissionName?: string) => { + if (!permissionName) { + return '' + } + + const suffix = permissionActionSuffixes.find((item) => permissionName.endsWith(`.${item}`)) + return suffix ? permissionName.slice(0, -suffix.length - 1) : permissionName +} + +const userDisplayName = (user: PermissionGrantedUserDto) => + user.fullName || + [user.name, user.surname].filter(Boolean).join(' ') || + user.userName || + user.email || + user.id + +const PermissionGranted = ({ + isOpen, + listFormCode, + permissionNames, + onClose, +}: PermissionGrantedProps) => { + const [loading, setLoading] = useState(false) + const [items, setItems] = useState([]) + const { translate } = useLocalization() + + const rootPermissionName = useMemo(() => { + const normalizedPermissionNames = unique(permissionNames) + return ( + normalizedPermissionNames.find( + (permissionName) => + !permissionActionSuffixes.some((suffix) => permissionName.endsWith(`.${suffix}`)), + ) ?? toRootPermissionName(normalizedPermissionNames[0]) + ) + }, [permissionNames.join('|')]) + + useEffect(() => { + if (!isOpen) { + return + } + + const loadData = async () => { + if (!rootPermissionName) { + setItems([]) + return + } + + setLoading(true) + try { + const response = await getPermissionGrantedUsers(rootPermissionName) + + setItems( + (response.data ?? []).sort((a, b) => { + if (!a.parentName && b.parentName) return -1 + if (a.parentName && !b.parentName) return 1 + return a.name.localeCompare(b.name) + }), + ) + } catch { + toast.push( + + {translate('::ListForms.ListForm.GrantedUsersErrorLoading')} + , + { placement: 'top-end' }, + ) + } finally { + setLoading(false) + } + } + + loadData() + }, [isOpen, rootPermissionName]) + + const uniqueUsers = useMemo(() => { + const map = new Map() + + items.forEach((permission) => { + permission.users.forEach((user) => { + const existing = map.get(user.id) + if (existing) { + existing.permissions.push(permissionActionLabel(permission.name)) + existing.roleNames = unique([...existing.roleNames, ...user.roleNames]) + } else { + map.set(user.id, { + ...user, + permissions: [permissionActionLabel(permission.name)], + }) + } + }) + }) + + return Array.from(map.values()).sort((a, b) => + userDisplayName(a).localeCompare(userDisplayName(b)), + ) + }, [items]) + + return ( + + +
+

+ {translate('::ListForms.ListForm.GrantedUsers')} +

+
+ {rootPermissionName || listFormCode} +
+
+ + {uniqueUsers.length} {translate('::ListForms.ListFormField.User')} + +
+ + + {loading ? ( +
+ +
+ ) : items.length === 0 ? ( +
+ {translate('::ListForms.ListForm.GrantedUsersNoAuthorization')} +
+ ) : ( +
+
+ + + + {translate('::ListForms.ListFormField.User')} + {translate('::App.Listform.ListformField.Email')} + {translate('::AbpIdentity.Roles')} + {translate('::Abp.Identity.User.Permissions')} + + + + {uniqueUsers.map((user) => ( + + +
+ +
+
+ {userDisplayName(user)} +
+
{user.userName}
+
+
+
+ {user.email} + +
+ {user.roleNames.map((roleName) => ( + R: {roleName} + ))} + {user.isDirectGrant && U} +
+
+ +
+ {unique(user.permissions).map((permission) => ( + {permission} + ))} +
+
+
+ ))} +
+
+
+
+ )} +
+ + + + +
+ ) +} + +export default PermissionGranted diff --git a/ui/src/views/list/useFilters.tsx b/ui/src/views/list/useFilters.tsx index 6c0a980..e8e77ce 100644 --- a/ui/src/views/list/useFilters.tsx +++ b/ui/src/views/list/useFilters.tsx @@ -176,6 +176,9 @@ const useFilters = ({ setIsCreateUpdateModalOpen: Dispatch> isDeleteModalOpen: boolean setIsDeleteModalOpen: Dispatch> + isPermissionUsersModalOpen: boolean + setIsPermissionUsersModalOpen: Dispatch> + permissionNames: string[] filtersForSelectBox: ISelectBoxData[] getFilters: () => Promise isImportModalOpen: boolean @@ -193,12 +196,22 @@ const useFilters = ({ const [isCreateUpdateModalOpen, setIsCreateUpdateModalOpen] = useState(false) const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) const [isImportModalOpen, setIsImportModalOpen] = useState(false) + const [isPermissionUsersModalOpen, setIsPermissionUsersModalOpen] = useState(false) const filteredGridPanelColor = 'rgba(10, 200, 10, 0.5)' // kullanici tanimli filtre ile filtrelenmis gridin paneline ait renk const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)' // kullanici tanimli gridState ile islem gormus gridin paneline ait renk const grdOpt = gridDto?.gridOptions const config = useStoreState((state) => state.abpConfig.config) + const permissionNames = [ + grdOpt?.permissionDto?.r, + grdOpt?.permissionDto?.c, + grdOpt?.permissionDto?.u, + grdOpt?.permissionDto?.d, + grdOpt?.permissionDto?.e, + grdOpt?.permissionDto?.i, + grdOpt?.permissionDto?.n, + ].filter(Boolean) as string[] const getFilters = async () => { const response = await getListFormCustomization( @@ -294,6 +307,14 @@ const useFilters = ({ } if (checkPermission('App.Listforms.Listform.Update')) { + if (permissionNames.length > 0) { + menus.push({ + text: translate('::ListForms.ListForm.GrantedUsers'), + id: 'permissionGrantedUsers', + icon: 'user', + }) + } + menus.push({ text: translate('::ListForms.ListForm.Manage'), id: 'openManage', @@ -387,6 +408,8 @@ const useFilters = ({ } else if (itemData.id === 'importManager') { // import modal aç setIsImportModalOpen(true) + } else if (itemData.id === 'permissionGrantedUsers') { + setIsPermissionUsersModalOpen(true) } }, // filtre menüsündeki elemanlar @@ -474,6 +497,9 @@ const useFilters = ({ setIsCreateUpdateModalOpen, isDeleteModalOpen, setIsDeleteModalOpen, + isPermissionUsersModalOpen, + setIsPermissionUsersModalOpen, + permissionNames, filtersForSelectBox, getFilters, isImportModalOpen,