Permission Granted Users

This commit is contained in:
Sedat ÖZTÜRK 2026-06-10 12:19:57 +03:00
parent 4e439f3bf5
commit 98add9e398
8 changed files with 452 additions and 1 deletions

View file

@ -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<string> RoleNames { get; set; } = [];
public List<PermissionGrantedUserDto> 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<string> RoleNames { get; set; } = [];
}

View file

@ -23,8 +23,10 @@ public class PlatformIdentityAppService : ApplicationService
{ {
public IIdentityUserAppService IdentityUserAppService { get; } public IIdentityUserAppService IdentityUserAppService { get; }
private readonly IIdentityUserRepository identityUserRepository; private readonly IIdentityUserRepository identityUserRepository;
private readonly IIdentityRoleRepository identityRoleRepository;
private readonly IIdentitySessionRepository identitySessionRepository; private readonly IIdentitySessionRepository identitySessionRepository;
private readonly IOpenIddictTokenManager openIddictTokenManager; private readonly IOpenIddictTokenManager openIddictTokenManager;
private readonly IPermissionGrantRepository permissionGrantRepository;
public IRepository<PermissionDefinitionRecord, Guid> permissionRepository { get; } public IRepository<PermissionDefinitionRecord, Guid> permissionRepository { get; }
public IRepository<Branch, Guid> branchRepository { get; } public IRepository<Branch, Guid> branchRepository { get; }
public IRepository<BranchUsers, Guid> branchUsersRepository { get; } public IRepository<BranchUsers, Guid> branchUsersRepository { get; }
@ -39,8 +41,10 @@ public class PlatformIdentityAppService : ApplicationService
public PlatformIdentityAppService( public PlatformIdentityAppService(
IIdentityUserAppService identityUserAppService, IIdentityUserAppService identityUserAppService,
IIdentityUserRepository identityUserRepository, IIdentityUserRepository identityUserRepository,
IIdentityRoleRepository identityRoleRepository,
IIdentitySessionRepository identitySessionRepository, IIdentitySessionRepository identitySessionRepository,
IOpenIddictTokenManager openIddictTokenManager, IOpenIddictTokenManager openIddictTokenManager,
IPermissionGrantRepository permissionGrantRepository,
IRepository<PermissionDefinitionRecord, Guid> permissionRepository, IRepository<PermissionDefinitionRecord, Guid> permissionRepository,
IRepository<Branch, Guid> branchRepository, IRepository<Branch, Guid> branchRepository,
IRepository<BranchUsers, Guid> branchUsersRepository, IRepository<BranchUsers, Guid> branchUsersRepository,
@ -54,8 +58,10 @@ public class PlatformIdentityAppService : ApplicationService
{ {
this.IdentityUserAppService = identityUserAppService; this.IdentityUserAppService = identityUserAppService;
this.identityUserRepository = identityUserRepository; this.identityUserRepository = identityUserRepository;
this.identityRoleRepository = identityRoleRepository;
this.identitySessionRepository = identitySessionRepository; this.identitySessionRepository = identitySessionRepository;
this.openIddictTokenManager = openIddictTokenManager; this.openIddictTokenManager = openIddictTokenManager;
this.permissionGrantRepository = permissionGrantRepository;
this.workHourRepository = workHourRepository; this.workHourRepository = workHourRepository;
this.departmentRepository = departmentRepository; this.departmentRepository = departmentRepository;
this.jobPositionRepository = jobPositionRepository; this.jobPositionRepository = jobPositionRepository;
@ -301,6 +307,118 @@ public class PlatformIdentityAppService : ApplicationService
return [.. list.OrderBy(p => p.Name)]; return [.. list.OrderBy(p => p.Name)];
} }
public async Task<List<PermissionGrantedUsersDto>> 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) public async Task CreateClaimUserAsync(UserClaimModel input)
{ {
var user = await identityUserRepository.GetAsync(input.UserId); var user = await identityUserRepository.GetAsync(input.UserId);

View file

@ -4236,6 +4236,24 @@
"en": "Reset list form structure", "en": "Reset list form structure",
"tr": "Liste form yapısını sıfırla" "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", "resourceName": "Platform",
"key": "ListForms.ListForm.Manage", "key": "ListForms.ListForm.Manage",

View file

@ -9,6 +9,7 @@ export interface TagProps extends CommonProps {
prefixClass?: string prefixClass?: string
suffix?: boolean | ReactNode suffix?: boolean | ReactNode
suffixClass?: string suffixClass?: string
title?: string
} }
const Tag = forwardRef<HTMLDivElement, TagProps>((props, ref) => { const Tag = forwardRef<HTMLDivElement, TagProps>((props, ref) => {
@ -19,11 +20,12 @@ const Tag = forwardRef<HTMLDivElement, TagProps>((props, ref) => {
suffix, suffix,
prefixClass, prefixClass,
suffixClass, suffixClass,
title,
...rest ...rest
} = props } = props
return ( return (
<div ref={ref} className={classNames('tag', className)} {...rest}> <div ref={ref} title={title} className={classNames('tag', className)} {...rest}>
{prefix && typeof prefix === 'boolean' && ( {prefix && typeof prefix === 'boolean' && (
<span <span
className={classNames('tag-affix tag-prefix', prefixClass)} className={classNames('tag-affix tag-prefix', prefixClass)}

View file

@ -16,6 +16,28 @@ export interface UserAvatarUpdateInput {
avatar?: File avatar?: File
} }
export interface PermissionGrantedUserDto {
tenantId: string
id: string
userName?: string
name?: string
surname?: string
fullName?: string
email?: string
isActive: boolean
isDirectGrant: boolean
roleNames: string[]
}
export interface PermissionGrantedUsersDto {
name: string
parentName?: string
displayName?: string
isEnabled: boolean
roleNames: string[]
users: PermissionGrantedUserDto[]
}
export const getRoles = (skipCount = 0, maxResultCount = 10) => export const getRoles = (skipCount = 0, maxResultCount = 10) =>
apiService.fetchData<ListResultDto<IdentityRoleDto>>({ apiService.fetchData<ListResultDto<IdentityRoleDto>>({
method: 'GET', method: 'GET',
@ -84,6 +106,13 @@ export const getPermissionsList = () =>
url: `/api/app/platform-identity/permission-list`, url: `/api/app/platform-identity/permission-list`,
}) })
export const getPermissionGrantedUsers = (permissionName: string) =>
apiService.fetchData<PermissionGrantedUsersDto[]>({
method: 'GET',
url: '/api/app/platform-identity/permission-granted-users',
params: { permissionName },
})
export const updatePermissions = ( export const updatePermissions = (
providerName: string, providerName: string,
providerKey: string, providerKey: string,

View file

@ -8,6 +8,7 @@ import { Dispatch, MutableRefObject, SetStateAction, useState } from 'react'
import CreatableSelect from 'react-select/creatable' import CreatableSelect from 'react-select/creatable'
import { ISelectBoxData } from './useFilters' import { ISelectBoxData } from './useFilters'
import { ListFormCustomizationTypeEnum } from '@/proxy/form/models' import { ListFormCustomizationTypeEnum } from '@/proxy/form/models'
import PermissionGranted from './PermissionGranted'
const GridFilterDialogs = (props: { const GridFilterDialogs = (props: {
listFormCode: string listFormCode: string
@ -17,6 +18,9 @@ const GridFilterDialogs = (props: {
setIsCreateUpdateModalOpen: Dispatch<SetStateAction<boolean>> setIsCreateUpdateModalOpen: Dispatch<SetStateAction<boolean>>
isDeleteModalOpen: boolean isDeleteModalOpen: boolean
setIsDeleteModalOpen: Dispatch<SetStateAction<boolean>> setIsDeleteModalOpen: Dispatch<SetStateAction<boolean>>
isPermissionUsersModalOpen: boolean
setIsPermissionUsersModalOpen: Dispatch<SetStateAction<boolean>>
permissionNames: string[]
getFilters: () => Promise<void> getFilters: () => Promise<void>
}) => { }) => {
const [newFilterName, setNewFilterName] = useState<string | undefined>() const [newFilterName, setNewFilterName] = useState<string | undefined>()
@ -30,6 +34,9 @@ const GridFilterDialogs = (props: {
setIsCreateUpdateModalOpen, setIsCreateUpdateModalOpen,
isDeleteModalOpen, isDeleteModalOpen,
setIsDeleteModalOpen, setIsDeleteModalOpen,
isPermissionUsersModalOpen,
setIsPermissionUsersModalOpen,
permissionNames,
getFilters, getFilters,
} = props } = props
@ -164,6 +171,13 @@ const GridFilterDialogs = (props: {
</Button> </Button>
</div> </div>
</Dialog> </Dialog>
<PermissionGranted
isOpen={isPermissionUsersModalOpen}
listFormCode={listFormCode}
permissionNames={permissionNames}
onClose={() => setIsPermissionUsersModalOpen(false)}
/>
</> </>
) )
} }

View file

@ -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<string, string> = {
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<PermissionGrantedUsersDto[]>([])
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(
<Notification type="danger" duration={3000}>
{translate('::ListForms.ListForm.GrantedUsersErrorLoading')}
</Notification>,
{ placement: 'top-end' },
)
} finally {
setLoading(false)
}
}
loadData()
}, [isOpen, rootPermissionName])
const uniqueUsers = useMemo(() => {
const map = new Map<string, PermissionGrantedUserDto & { permissions: string[] }>()
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 (
<Dialog isOpen={isOpen} onClose={onClose} onRequestClose={onClose} width={1000}>
<Dialog.Header className="flex min-h-12 items-center justify-between gap-3 border-b border-gray-200 px-2 py-2 dark:border-gray-700">
<div className="min-w-0">
<h2 className="truncate text-lg font-semibold text-gray-900 dark:text-gray-100">
{translate('::ListForms.ListForm.GrantedUsers')}
</h2>
<div className="truncate text-xs text-gray-500 dark:text-gray-400">
{rootPermissionName || listFormCode}
</div>
</div>
<Tag>
{uniqueUsers.length} {translate('::ListForms.ListFormField.User')}
</Tag>
</Dialog.Header>
<Dialog.Body className="max-h-[70vh] overflow-auto px-0 pb-0 pt-4">
{loading ? (
<div className="flex min-h-48 items-center justify-center">
<Spinner size={36} />
</div>
) : items.length === 0 ? (
<div className="px-6 py-10 text-center text-sm text-gray-500 dark:text-gray-400">
{translate('::ListForms.ListForm.GrantedUsersNoAuthorization')}
</div>
) : (
<div className="space-y-5">
<div className="overflow-x-auto rounded border border-gray-200 dark:border-gray-700 mb-5">
<Table compact>
<Table.THead>
<Table.Tr>
<Table.Th>{translate('::ListForms.ListFormField.User')}</Table.Th>
<Table.Th>{translate('::App.Listform.ListformField.Email')}</Table.Th>
<Table.Th>{translate('::AbpIdentity.Roles')}</Table.Th>
<Table.Th>{translate('::Abp.Identity.User.Permissions')}</Table.Th>
</Table.Tr>
</Table.THead>
<Table.TBody>
{uniqueUsers.map((user) => (
<Table.Tr key={user.id}>
<Table.Td>
<div className="flex min-w-0 items-center gap-2">
<Avatar size={28} shape="circle" src={AVATAR_URL(user.id, user.tenantId)} />
<div className="min-w-0">
<div className="truncate font-medium text-gray-900 dark:text-gray-100">
{userDisplayName(user)}
</div>
<div className="truncate text-xs text-gray-500">{user.userName}</div>
</div>
</div>
</Table.Td>
<Table.Td>{user.email}</Table.Td>
<Table.Td>
<div className="flex flex-wrap gap-1">
{user.roleNames.map((roleName) => (
<Tag key={roleName}>R: {roleName}</Tag>
))}
{user.isDirectGrant && <Tag>U</Tag>}
</div>
</Table.Td>
<Table.Td>
<div className="flex flex-wrap gap-1">
{unique(user.permissions).map((permission) => (
<Tag key={permission} title={permission}>{permission}</Tag>
))}
</div>
</Table.Td>
</Table.Tr>
))}
</Table.TBody>
</Table>
</div>
</div>
)}
</Dialog.Body>
<Dialog.Footer className="border-t border-gray-200 pt-4 text-right dark:border-gray-700">
<Button variant='solid' onClick={onClose}>{translate('::Close')}</Button>
</Dialog.Footer>
</Dialog>
)
}
export default PermissionGranted

View file

@ -176,6 +176,9 @@ const useFilters = ({
setIsCreateUpdateModalOpen: Dispatch<SetStateAction<boolean>> setIsCreateUpdateModalOpen: Dispatch<SetStateAction<boolean>>
isDeleteModalOpen: boolean isDeleteModalOpen: boolean
setIsDeleteModalOpen: Dispatch<SetStateAction<boolean>> setIsDeleteModalOpen: Dispatch<SetStateAction<boolean>>
isPermissionUsersModalOpen: boolean
setIsPermissionUsersModalOpen: Dispatch<SetStateAction<boolean>>
permissionNames: string[]
filtersForSelectBox: ISelectBoxData[] filtersForSelectBox: ISelectBoxData[]
getFilters: () => Promise<void> getFilters: () => Promise<void>
isImportModalOpen: boolean isImportModalOpen: boolean
@ -193,12 +196,22 @@ const useFilters = ({
const [isCreateUpdateModalOpen, setIsCreateUpdateModalOpen] = useState(false) const [isCreateUpdateModalOpen, setIsCreateUpdateModalOpen] = useState(false)
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
const [isImportModalOpen, setIsImportModalOpen] = 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 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 statedGridPanelColor = 'rgba(50, 200, 200, 0.5)' // kullanici tanimli gridState ile islem gormus gridin paneline ait renk
const grdOpt = gridDto?.gridOptions const grdOpt = gridDto?.gridOptions
const config = useStoreState((state) => state.abpConfig.config) 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 getFilters = async () => {
const response = await getListFormCustomization( const response = await getListFormCustomization(
@ -294,6 +307,14 @@ const useFilters = ({
} }
if (checkPermission('App.Listforms.Listform.Update')) { if (checkPermission('App.Listforms.Listform.Update')) {
if (permissionNames.length > 0) {
menus.push({
text: translate('::ListForms.ListForm.GrantedUsers'),
id: 'permissionGrantedUsers',
icon: 'user',
})
}
menus.push({ menus.push({
text: translate('::ListForms.ListForm.Manage'), text: translate('::ListForms.ListForm.Manage'),
id: 'openManage', id: 'openManage',
@ -387,6 +408,8 @@ const useFilters = ({
} else if (itemData.id === 'importManager') { } else if (itemData.id === 'importManager') {
// import modal aç // import modal aç
setIsImportModalOpen(true) setIsImportModalOpen(true)
} else if (itemData.id === 'permissionGrantedUsers') {
setIsPermissionUsersModalOpen(true)
} }
}, },
// filtre menüsündeki elemanlar // filtre menüsündeki elemanlar
@ -474,6 +497,9 @@ const useFilters = ({
setIsCreateUpdateModalOpen, setIsCreateUpdateModalOpen,
isDeleteModalOpen, isDeleteModalOpen,
setIsDeleteModalOpen, setIsDeleteModalOpen,
isPermissionUsersModalOpen,
setIsPermissionUsersModalOpen,
permissionNames,
filtersForSelectBox, filtersForSelectBox,
getFilters, getFilters,
isImportModalOpen, isImportModalOpen,