|
|
@@ -71,7 +71,11 @@ import { Textarea } from '@/components/ui/textarea'
|
|
|
import { PERMISSION_CODES, hasPermissionCode } from '@/lib/admin-permissions'
|
|
|
import { useAuthStore } from '@/stores/auth-store'
|
|
|
|
|
|
-import { getPermissions, type PermissionItem } from '../permissions/api'
|
|
|
+import {
|
|
|
+ getPermissions,
|
|
|
+ getPermissionsWithRole,
|
|
|
+ type PermissionItem,
|
|
|
+} from '../permissions/api'
|
|
|
import { getRoles, getRole, createRole, updateRole, deleteRole } from './api'
|
|
|
import type { Role } from './types'
|
|
|
|
|
|
@@ -178,22 +182,75 @@ function getPermissionResourceKey(permission: PermissionItem) {
|
|
|
return key.includes('.') ? key.split('.')[0] : key
|
|
|
}
|
|
|
|
|
|
+function getPermissionResourceKeyFromCode(key: string) {
|
|
|
+ if (!key.startsWith('route.')) {
|
|
|
+ return key.includes('.') ? key.split('.')[0] : key
|
|
|
+ }
|
|
|
+
|
|
|
+ const routePermissionMatch = key.match(/^route\.(.+)\.view$/)
|
|
|
+ return routePermissionMatch?.[1] ?? key.split('.')[1] ?? key
|
|
|
+}
|
|
|
+
|
|
|
+function getRoutePermissionResourceKey(permission: PermissionItem) {
|
|
|
+ return getPermissionResourceKeyFromCode(getPermissionKey(permission))
|
|
|
+}
|
|
|
+
|
|
|
function getPermissionModuleKey(
|
|
|
permission: PermissionItem,
|
|
|
actions: RolePermissionModule['actions']
|
|
|
) {
|
|
|
- const actionKey = actions.find((action) => action.key.includes('.'))?.key
|
|
|
- if (actionKey) return actionKey.split('.')[0]
|
|
|
+ const routeResourceKey = getRoutePermissionResourceKey(permission)
|
|
|
+ if (routeResourceKey !== getPermissionResourceKey(permission)) {
|
|
|
+ return routeResourceKey
|
|
|
+ }
|
|
|
+
|
|
|
+ const action = actions.find((item) => item.key.includes('.'))
|
|
|
+ if (action) return getPermissionResourceKeyFromCode(action.key)
|
|
|
|
|
|
const key = getPermissionKey(permission)
|
|
|
if (key.startsWith('route.')) {
|
|
|
- const parts = key.split('.')
|
|
|
- return parts[1] ?? key
|
|
|
+ return getRoutePermissionResourceKey(permission)
|
|
|
}
|
|
|
|
|
|
return getPermissionResourceKey(permission)
|
|
|
}
|
|
|
|
|
|
+function permissionHasRole(permission: PermissionItem) {
|
|
|
+ return (
|
|
|
+ permission.has_permission ??
|
|
|
+ permission.checked ??
|
|
|
+ permission.selected ??
|
|
|
+ false
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+function toRoleFormPermissions(permissions: PermissionItem[]) {
|
|
|
+ return toRolePermissionModules(permissions).reduce<RoleFormValues['permissions']>(
|
|
|
+ (formPermissions, module) => {
|
|
|
+ const sourceModule = permissions.find(
|
|
|
+ (permission) =>
|
|
|
+ getPermissionModuleKey(permission, module.actions) === module.key
|
|
|
+ )
|
|
|
+ const actionsSource =
|
|
|
+ sourceModule?.module_relation_permissions ??
|
|
|
+ sourceModule?.actions ??
|
|
|
+ sourceModule?.children ??
|
|
|
+ sourceModule?.permissions ??
|
|
|
+ []
|
|
|
+ formPermissions[module.key] = actionsSource
|
|
|
+ .filter(
|
|
|
+ (action) =>
|
|
|
+ permissionHasRole(action) ||
|
|
|
+ permissionHasRole(action.ExtPermission ?? action)
|
|
|
+ )
|
|
|
+ .map((action) => getPermissionKey(action.ExtPermission ?? action))
|
|
|
+ .filter((key) => key)
|
|
|
+ return formPermissions
|
|
|
+ },
|
|
|
+ {}
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
function getViewPermissionKey(module: RolePermissionModule) {
|
|
|
const viewAction = module.actions.find(
|
|
|
(action) => action.key.endsWith('.view') || action.key === 'view'
|
|
|
@@ -201,6 +258,60 @@ function getViewPermissionKey(module: RolePermissionModule) {
|
|
|
return viewAction?.key ?? module.viewPermissionKey ?? `${module.key}.view`
|
|
|
}
|
|
|
|
|
|
+function getOwnDataViewPermissionKey(module: RolePermissionModule) {
|
|
|
+ return module.actions.find((action) => isOwnDataViewAction(action))?.key
|
|
|
+}
|
|
|
+
|
|
|
+function getAllDataViewPermissionKey(module: RolePermissionModule) {
|
|
|
+ return module.actions.find((action) => isAllDataViewAction(action))?.key
|
|
|
+}
|
|
|
+
|
|
|
+function isOwnDataViewAction(action: RolePermissionModule['actions'][number]) {
|
|
|
+ const actionKey = action.key.toLowerCase()
|
|
|
+ const actionLabel = action.label.toLowerCase()
|
|
|
+
|
|
|
+ if (actionLabel.includes('仅自己') || actionLabel.includes('only mine')) {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ if (actionLabel.includes('全部数据') || actionLabel.includes('all data')) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ actionKey.includes('view_self') ||
|
|
|
+ actionKey.includes('view_own') ||
|
|
|
+ actionKey.includes('read_self') ||
|
|
|
+ actionKey.includes('read_own') ||
|
|
|
+ actionKey.includes('only_mine') ||
|
|
|
+ actionKey.includes('.self.') ||
|
|
|
+ actionKey.includes('.own.') ||
|
|
|
+ actionKey.endsWith('.self') ||
|
|
|
+ actionKey.endsWith('.own') ||
|
|
|
+ actionLabel.includes('own data') ||
|
|
|
+ actionLabel.includes('self data')
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+function isAllDataViewAction(action: RolePermissionModule['actions'][number]) {
|
|
|
+ const actionKey = action.key.toLowerCase()
|
|
|
+ const actionLabel = action.label.toLowerCase()
|
|
|
+
|
|
|
+ if (actionLabel.includes('全部数据') || actionLabel.includes('all data')) {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ if (actionLabel.includes('仅自己') || actionLabel.includes('only mine')) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ actionKey.includes('view_all') ||
|
|
|
+ actionKey.includes('read_all') ||
|
|
|
+ actionKey.includes('all_data') ||
|
|
|
+ actionKey.includes('.all.') ||
|
|
|
+ actionKey.endsWith('.all')
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
function isRolePermissionModule(module: RolePermissionModule) {
|
|
|
return module.key === 'role' || module.key === 'roles'
|
|
|
}
|
|
|
@@ -254,6 +365,105 @@ function getPermissionTitle(permission: PermissionItem, fallback: string) {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
+const PERMISSION_MODULE_TITLE_MAP: Record<string, string> = {
|
|
|
+ dashboard: 'Dashboard',
|
|
|
+ channel: 'Channel Management',
|
|
|
+ models: 'Model Management',
|
|
|
+ model: 'Model Management',
|
|
|
+ user: 'User Management',
|
|
|
+ role: 'Platform Roles',
|
|
|
+ tenant: 'Tenant Management',
|
|
|
+ finance: 'Finance Management',
|
|
|
+ billing: 'Finance Management',
|
|
|
+ wallet: 'Finance Management',
|
|
|
+ pricing: 'Pricing',
|
|
|
+ key: 'Keys',
|
|
|
+ keys: 'Keys',
|
|
|
+ profile: 'Profile',
|
|
|
+ settings: 'Settings',
|
|
|
+ 'usage-logs': 'Usage Logs',
|
|
|
+ 'audit-logs': 'Audit Logs',
|
|
|
+}
|
|
|
+
|
|
|
+const PERMISSION_ACTION_TITLE_MAP: Record<string, string> = {
|
|
|
+ view: 'View',
|
|
|
+ create: 'Create',
|
|
|
+ update: 'Edit',
|
|
|
+ edit: 'Edit',
|
|
|
+ delete: 'Delete',
|
|
|
+ disable: 'Disable',
|
|
|
+ enable: 'Enable',
|
|
|
+ export: 'Export',
|
|
|
+ audit: 'Audit',
|
|
|
+ all: 'All',
|
|
|
+ own: 'Own',
|
|
|
+}
|
|
|
+
|
|
|
+const PERMISSION_MODULE_FALLBACK_MAP: Record<string, string> = {
|
|
|
+ 用户管理: 'User Management',
|
|
|
+ 平台角色: 'Platform Roles',
|
|
|
+ 租户管理: 'Tenant Management',
|
|
|
+ 渠道管理: 'Channel Management',
|
|
|
+ 模型管理: 'Model Management',
|
|
|
+ 财务管理: 'Finance Management',
|
|
|
+}
|
|
|
+
|
|
|
+const PERMISSION_ACTION_FALLBACK_MAP: Record<string, string> = {
|
|
|
+ 查看: 'View',
|
|
|
+ 新增: 'Create',
|
|
|
+ 编辑: 'Edit',
|
|
|
+ 删除: 'Delete',
|
|
|
+ 禁用: 'Disable',
|
|
|
+ 启用: 'Enable',
|
|
|
+ 导出: 'Export',
|
|
|
+ 审计: 'Audit',
|
|
|
+}
|
|
|
+
|
|
|
+function getPermissionModuleSegment(permission: PermissionItem) {
|
|
|
+ const moduleCode =
|
|
|
+ permission.module_code ??
|
|
|
+ permission.permission_code ??
|
|
|
+ permission.permission_key ??
|
|
|
+ permission.front_perm_code ??
|
|
|
+ ''
|
|
|
+
|
|
|
+ return moduleCode
|
|
|
+ .replace(/^route\./, '')
|
|
|
+ .split('.')
|
|
|
+ .filter(Boolean)[0]
|
|
|
+}
|
|
|
+
|
|
|
+function getPermissionActionSegment(permission: PermissionItem) {
|
|
|
+ const actionCode =
|
|
|
+ permission.front_perm_code ??
|
|
|
+ permission.permission_code ??
|
|
|
+ permission.permission_key ??
|
|
|
+ ''
|
|
|
+
|
|
|
+ const segments = actionCode.split('.').filter(Boolean)
|
|
|
+ return segments[segments.length - 1]
|
|
|
+}
|
|
|
+
|
|
|
+function getLocalizedPermissionModuleTitle(permission: PermissionItem, fallback: string) {
|
|
|
+ const moduleSegment = getPermissionModuleSegment(permission)
|
|
|
+ if (moduleSegment && PERMISSION_MODULE_TITLE_MAP[moduleSegment]) {
|
|
|
+ return PERMISSION_MODULE_TITLE_MAP[moduleSegment]
|
|
|
+ }
|
|
|
+
|
|
|
+ const rawTitle = getPermissionTitle(permission, fallback)
|
|
|
+ return PERMISSION_MODULE_FALLBACK_MAP[rawTitle] ?? rawTitle
|
|
|
+}
|
|
|
+
|
|
|
+function getLocalizedPermissionActionTitle(permission: PermissionItem, fallback: string) {
|
|
|
+ const actionSegment = getPermissionActionSegment(permission)
|
|
|
+ if (actionSegment && PERMISSION_ACTION_TITLE_MAP[actionSegment]) {
|
|
|
+ return PERMISSION_ACTION_TITLE_MAP[actionSegment]
|
|
|
+ }
|
|
|
+
|
|
|
+ const rawTitle = getPermissionTitle(permission, fallback)
|
|
|
+ return PERMISSION_ACTION_FALLBACK_MAP[rawTitle] ?? rawTitle
|
|
|
+}
|
|
|
+
|
|
|
function getPermissionDescription(permission: PermissionItem) {
|
|
|
return (
|
|
|
permission.module_desc ??
|
|
|
@@ -303,7 +513,7 @@ function toRolePermissionModules(permissions: PermissionItem[]) {
|
|
|
return {
|
|
|
id: getPermissionId(permissionAction),
|
|
|
key: getPermissionKey(permissionAction),
|
|
|
- label: getPermissionTitle(
|
|
|
+ label: getLocalizedPermissionActionTitle(
|
|
|
permissionAction,
|
|
|
getPermissionKey(permissionAction)
|
|
|
),
|
|
|
@@ -315,7 +525,7 @@ function toRolePermissionModules(permissions: PermissionItem[]) {
|
|
|
return {
|
|
|
id: getPermissionId(permission),
|
|
|
key,
|
|
|
- title: getPermissionTitle(permission, key),
|
|
|
+ title: getLocalizedPermissionModuleTitle(permission, key),
|
|
|
description: getPermissionDescription(permission),
|
|
|
viewPermissionKey: getPermissionKey(permission),
|
|
|
actions,
|
|
|
@@ -379,6 +589,15 @@ export function RolesSettings() {
|
|
|
permissions: result.data.permissions ?? {},
|
|
|
})
|
|
|
} catch {}
|
|
|
+
|
|
|
+ try {
|
|
|
+ const permissions = await getPermissionsWithRole(role.id)
|
|
|
+ form.reset({
|
|
|
+ name: role.name,
|
|
|
+ description: role.description || '',
|
|
|
+ permissions: toRoleFormPermissions(permissions),
|
|
|
+ })
|
|
|
+ } catch {}
|
|
|
}
|
|
|
|
|
|
// #region debug-point role-actions-open-state
|
|
|
@@ -745,7 +964,7 @@ export function RolesSettings() {
|
|
|
} catch {}
|
|
|
setLocalRoles((prev) => [
|
|
|
{
|
|
|
- id: Math.max(0, ...prev.map((role) => role.id)) + 1,
|
|
|
+ id: Math.max(0, ...prev.map((role) => Number(role.id) || 0)) + 1,
|
|
|
name: submitData.name,
|
|
|
description: submitData.description,
|
|
|
permission_modules_count: Object.keys(submitData.permissions)
|
|
|
@@ -873,6 +1092,10 @@ export function RolesSettings() {
|
|
|
selectableActionKeys.includes(permission)
|
|
|
)
|
|
|
const viewPermissionKey = getViewPermissionKey(module)
|
|
|
+ const ownDataViewPermissionKey =
|
|
|
+ getOwnDataViewPermissionKey(module)
|
|
|
+ const allDataViewPermissionKey =
|
|
|
+ getAllDataViewPermissionKey(module)
|
|
|
const isAllSelected =
|
|
|
selectableActionKeys.length > 0 &&
|
|
|
selectableActionKeys.every((actionKey) =>
|
|
|
@@ -905,17 +1128,21 @@ export function RolesSettings() {
|
|
|
/>
|
|
|
</div>
|
|
|
{module.actions.length ? (
|
|
|
- <div className='grid grid-cols-2 gap-2 sm:grid-cols-4'>
|
|
|
+ <div className='flex flex-wrap gap-2'>
|
|
|
{module.actions.map((action) => {
|
|
|
const isViewAction =
|
|
|
action.key === viewPermissionKey
|
|
|
+ const isOwnDataViewAction =
|
|
|
+ action.key === ownDataViewPermissionKey
|
|
|
+ const isAllDataViewAction =
|
|
|
+ action.key === allDataViewPermissionKey
|
|
|
const isSelectable =
|
|
|
isPermissionActionSelectable(module, action)
|
|
|
|
|
|
return (
|
|
|
<label
|
|
|
key={`${module.key}-${action.key}`}
|
|
|
- className={`flex items-center gap-2 rounded-md border px-3 py-2 text-sm ${isSelectable ? 'hover:bg-accent cursor-pointer' : 'cursor-not-allowed opacity-50'}`}
|
|
|
+ className={`inline-flex w-fit items-center gap-2 rounded-md border px-3 py-2 text-sm ${isSelectable ? 'hover:bg-accent cursor-pointer' : 'cursor-not-allowed opacity-50'}`}
|
|
|
>
|
|
|
<Checkbox
|
|
|
checked={
|
|
|
@@ -930,15 +1157,32 @@ export function RolesSettings() {
|
|
|
(key) => key !== action.key
|
|
|
)
|
|
|
if (checked) {
|
|
|
+ const nextSelectedActions = [
|
|
|
+ ...selectedActions,
|
|
|
+ viewPermissionKey,
|
|
|
+ action.key,
|
|
|
+ ]
|
|
|
+ if (
|
|
|
+ isAllDataViewAction &&
|
|
|
+ ownDataViewPermissionKey
|
|
|
+ ) {
|
|
|
+ nextSelectedActions.push(
|
|
|
+ ownDataViewPermissionKey
|
|
|
+ )
|
|
|
+ }
|
|
|
nextActions = [
|
|
|
- ...new Set([
|
|
|
- ...selectedActions,
|
|
|
- viewPermissionKey,
|
|
|
- action.key,
|
|
|
- ]),
|
|
|
+ ...new Set(nextSelectedActions),
|
|
|
]
|
|
|
} else if (isViewAction) {
|
|
|
nextActions = []
|
|
|
+ } else if (
|
|
|
+ isOwnDataViewAction &&
|
|
|
+ allDataViewPermissionKey
|
|
|
+ ) {
|
|
|
+ nextActions = nextActions.filter(
|
|
|
+ (key) =>
|
|
|
+ key !== allDataViewPermissionKey
|
|
|
+ )
|
|
|
}
|
|
|
field.onChange({
|
|
|
...field.value,
|
|
|
@@ -946,7 +1190,9 @@ export function RolesSettings() {
|
|
|
})
|
|
|
}}
|
|
|
/>
|
|
|
- <span>{t(action.label)}</span>
|
|
|
+ <span className='whitespace-nowrap'>
|
|
|
+ {t(action.label)}
|
|
|
+ </span>
|
|
|
</label>
|
|
|
)
|
|
|
})}
|