1.修复角色组不能选择权限的报错

2.修复角色创建子角色报权限不够的问题
This commit is contained in:
2026-05-29 10:06:10 +08:00
parent 2140b37dfd
commit eba80b1bf4
7 changed files with 194 additions and 51 deletions

View File

@@ -253,28 +253,20 @@ class Admin extends Backend
if (!$data) { if (!$data) {
return $this->error(__('Parameter %s can not be empty', [''])); return $this->error(__('Parameter %s can not be empty', ['']));
} }
$data = $this->normalizeSingleGroup($data); $isSelfEdit = (int) $this->auth->id === (int) $id;
if (!$this->hasSingleGroup($data['group_arr'] ?? null)) { if ($isSelfEdit) {
return $this->error(__('Please select exactly one role group')); unset($data['group_arr'], $data['group_name_arr']);
} }
$postedGroups = array_map('intval', $data['group_arr'] ?? []); $editGroupArr = null;
$rowGroups = array_map('intval', $row->group_arr ?? []); if (array_key_exists('group_arr', $data)) {
sort($postedGroups); $data = $this->normalizeSingleGroup($data);
sort($rowGroups); if (!$this->hasSingleGroup($data['group_arr'] ?? null)) {
return $this->error(__('Please select exactly one role group'));
// 当前管理员编辑自身时,不允许修改角色组
if ((int)$this->auth->id === (int)$id) {
$postedGroups = $data['group_arr'] ?? [];
if (!is_array($postedGroups)) {
$postedGroups = [];
}
$originGroups = $row->group_arr ?? [];
sort($postedGroups);
sort($originGroups);
if ($postedGroups !== $originGroups) {
return $this->error(__('You cannot modify your own management group!'));
} }
$editGroupArr = $data['group_arr'];
} elseif (!$isSelfEdit) {
return $this->error(__('Please select exactly one role group'));
} }
if ($this->modelValidate) { if ($this->modelValidate) {
@@ -285,8 +277,10 @@ class Admin extends Backend
'password' => 'nullable|string|regex:/^(?!.*[&<>"\'\n\r]).{6,32}$/', 'password' => 'nullable|string|regex:/^(?!.*[&<>"\'\n\r]).{6,32}$/',
'email' => 'email|unique:admin,email,' . $id, 'email' => 'email|unique:admin,email,' . $id,
'mobile' => 'regex:/^1[3-9]\d{9}$/|unique:admin,mobile,' . $id, 'mobile' => 'regex:/^1[3-9]\d{9}$/|unique:admin,mobile,' . $id,
'group_arr' => 'required|array',
]; ];
if (array_key_exists('group_arr', $data)) {
$rules['group_arr'] = 'required|array';
}
$messages = [ $messages = [
'username.regex' => __('Please input correct username'), 'username.regex' => __('Please input correct username'),
'password.regex' => __('Please input correct password'), 'password.regex' => __('Please input correct password'),
@@ -306,10 +300,10 @@ class Admin extends Backend
} }
$groupAccess = []; $groupAccess = [];
if (!empty($data['group_arr'])) { if (!$isSelfEdit && !empty($editGroupArr)) {
$checkGroups = []; $checkGroups = [];
$rowGroupArr = $row->group_arr ?? []; $rowGroupArr = $row->group_arr ?? [];
foreach ($data['group_arr'] as $datum) { foreach ($editGroupArr as $datum) {
if (!in_array($datum, $rowGroupArr)) { if (!in_array($datum, $rowGroupArr)) {
$checkGroups[] = $datum; $checkGroups[] = $datum;
} }
@@ -323,32 +317,36 @@ class Admin extends Backend
} }
$data = $this->excludeFields($data); $data = $this->excludeFields($data);
unset($data['invite_code']); unset($data['invite_code'], $data['group_arr'], $data['group_name_arr']);
$creatorChannelId = $this->getCreatorChannelId(); if (!$isSelfEdit && $editGroupArr !== null) {
$groupChannelId = $this->resolveChannelIdFromPrimaryGroup($data['group_arr'] ?? []); $creatorChannelId = $this->getCreatorChannelId();
if (!$this->auth->isSuperAdmin()) { $groupChannelId = $this->resolveChannelIdFromPrimaryGroup($editGroupArr);
if ($creatorChannelId === null || $creatorChannelId === '') { if (!$this->auth->isSuperAdmin()) {
return $this->error(__('You have no permission')); if ($creatorChannelId === null || $creatorChannelId === '') {
return $this->error(__('You have no permission'));
}
if ($groupChannelId === null || $groupChannelId === '') {
return $this->error(__('Selected role group is not bound to a channel'));
}
if ((string) $groupChannelId !== (string) $creatorChannelId) {
return $this->error(__('Selected role group channel does not match current account'));
}
$data['channel_id'] = $creatorChannelId;
} else {
$data['channel_id'] = ($groupChannelId === null || $groupChannelId === '') ? null : $groupChannelId;
} }
if ($groupChannelId === null || $groupChannelId === '') {
return $this->error(__('Selected role group is not bound to a channel'));
}
if ((string) $groupChannelId !== (string) $creatorChannelId) {
return $this->error(__('Selected role group channel does not match current account'));
}
$data['channel_id'] = $creatorChannelId;
} else {
$data['channel_id'] = ($groupChannelId === null || $groupChannelId === '') ? null : $groupChannelId;
} }
$result = false; $result = false;
$this->model->startTrans(); $this->model->startTrans();
try { try {
$result = $row->save($data); $result = $row->save($data);
Db::name('admin_group_access') if (!$isSelfEdit) {
->where('uid', $id) Db::name('admin_group_access')
->delete(); ->where('uid', $id)
if ($groupAccess) { ->delete();
Db::name('admin_group_access')->insertAll($groupAccess); if ($groupAccess) {
Db::name('admin_group_access')->insertAll($groupAccess);
}
} }
$this->model->commit(); $this->model->commit();
} catch (Throwable $e) { } catch (Throwable $e) {

View File

@@ -19,6 +19,11 @@ class Group extends Backend
{ {
protected string $authMethod = 'allAuthAndOthers'; protected string $authMethod = 'allAuthAndOthers';
/**
* 角色组表单分配权限树(仅需登录 + 具备角色组管理相关权限,不依赖菜单规则管理权限)
*/
protected array $noNeedPermission = ['rules'];
protected ?object $model = null; protected ?object $model = null;
protected string|array $preExcludeFields = ['create_time', 'update_time']; protected string|array $preExcludeFields = ['create_time', 'update_time'];
@@ -90,7 +95,7 @@ class Group extends Backend
if ($inheritRes !== null) { if ($inheritRes !== null) {
return $inheritRes; return $inheritRes;
} }
$rulesRes = $this->handleRules($data); $rulesRes = $this->handleRules($data, $pidInt);
if ($rulesRes instanceof Response) return $rulesRes; if ($rulesRes instanceof Response) return $rulesRes;
$result = false; $result = false;
@@ -161,7 +166,7 @@ class Group extends Backend
if ($inheritRes !== null) { if ($inheritRes !== null) {
return $inheritRes; return $inheritRes;
} }
$rulesRes = $this->handleRules($data); $rulesRes = $this->handleRules($data, $pidInt);
if ($rulesRes instanceof Response) return $rulesRes; if ($rulesRes instanceof Response) return $rulesRes;
$result = false; $result = false;
@@ -296,9 +301,29 @@ class Group extends Backend
} }
/** /**
* 当前登录管理员可分配给下级角色组的菜单权限树(与 Rule::getMenus 一致,走角色组管理权限)
*/
public function rules(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
if (!$this->auth->isSuperAdmin() && !$this->canManageRoleGroups()) {
return $this->error(__('You have no permission'), [], 401);
}
return $this->success('', [
'list' => $this->getAssignableMenuRules($request),
]);
}
/**
* @param int $pidInt 上级角色组 ID大于 0 表示创建/编辑的是下级组,可与当前管理员拥有相同菜单权限
* @return array|Response * @return array|Response
*/ */
private function handleRules(array &$data) private function handleRules(array &$data, int $pidInt = 0)
{ {
if (!empty($data['rules']) && is_array($data['rules'])) { if (!empty($data['rules']) && is_array($data['rules'])) {
$superAdmin = true; $superAdmin = true;
@@ -310,7 +335,6 @@ class Group extends Backend
$checkedRules[] = $postRuleId; $checkedRules[] = $postRuleId;
} }
} }
foreach ($allRuleIds as $ruleId) { foreach ($allRuleIds as $ruleId) {
if (!in_array($ruleId, $checkedRules)) { if (!in_array($ruleId, $checkedRules)) {
$superAdmin = false; $superAdmin = false;
@@ -320,9 +344,15 @@ class Group extends Backend
if ($superAdmin && $this->auth->isSuperAdmin()) { if ($superAdmin && $this->auth->isSuperAdmin()) {
$data['rules'] = '*'; $data['rules'] = '*';
} else { } else {
$ownedRuleIds = $this->auth->getRuleIds(); $ownedRuleIds = $this->normalizeRuleIds($this->auth->getRuleIds());
$checkedRules = $this->normalizeRuleIds($checkedRules);
if (!array_diff($ownedRuleIds, $checkedRules)) { // 仅限制「非下级」角色组防止子管理员新建与自己平级的全权限组下级组pid>0允许授予相同菜单权限
if (
$pidInt <= 0
&& $ownedRuleIds !== []
&& !array_diff($ownedRuleIds, $checkedRules)
) {
return $this->error(__('Role group has all your rights, please contact the upper administrator to add or do not need to add!')); return $this->error(__('Role group has all your rights, please contact the upper administrator to add or do not need to add!'));
} }
@@ -338,6 +368,23 @@ class Group extends Backend
return $data; return $data;
} }
/**
* @param array<int|string> $ids
* @return array<int>
*/
private function normalizeRuleIds(array $ids): array
{
$result = [];
foreach ($ids as $id) {
if ($id === '*' || $id === '' || $id === null) {
continue;
}
$result[] = (int) $id;
}
return array_values(array_unique($result));
}
private function getGroups(Request $request, array $where = []): array private function getGroups(Request $request, array $where = []): array
{ {
$pk = $this->model->getPk(); $pk = $this->model->getPk();
@@ -500,4 +547,82 @@ class Group extends Backend
return null; return null;
} }
private function canManageRoleGroups(): bool
{
foreach (['auth/group/index', 'auth/group/add', 'auth/group/edit', 'auth/Group/index', 'auth/Group/add', 'auth/Group/edit'] as $routePath) {
if ($this->auth->check($routePath)) {
return true;
}
}
return false;
}
/**
* @return array<int, array<string, mixed>>
*/
private function getAssignableMenuRules(Request $request): array
{
$ids = $this->auth->getRuleIds();
$where = [];
if (!in_array('*', $ids, true)) {
$where[] = ['id', 'in', $ids ?: [0]];
}
$rules = (new AdminRule())
->where($where)
->order(['weigh' => 'desc'])
->select()
->toArray();
$toEnglish = !$this->shouldForceMenuTitleZh($request) && $this->shouldTranslateMenuToEnglish();
foreach ($rules as $idx => $rule) {
$title = $rule['title'] ?? '';
if (is_string($title) && $title !== '') {
$rules[$idx]['title'] = $toEnglish ? $this->menuTitleToEn($title) : $this->menuTitleToZh($title);
}
}
return $this->tree->assembleChild($rules);
}
private function shouldTranslateMenuToEnglish(): bool
{
$lang = function_exists('locale') ? locale() : '';
$normalized = is_string($lang) ? strtolower(str_replace('_', '-', trim($lang))) : '';
return str_starts_with($normalized, 'en');
}
private function shouldForceMenuTitleZh(Request $request): bool
{
$flag = $request->get('force_menu_zh') ?? $request->post('force_menu_zh');
return in_array($flag, [1, '1', true, 'true', 'yes', 'on'], true);
}
private function menuTitleToZh(string $title): string
{
static $zhMap = null;
if (!is_array($zhMap)) {
$mapFile = app_path() . '/common/lang/zh-cn/admin_rule_title.php';
$loaded = is_file($mapFile) ? include $mapFile : [];
$zhMap = is_array($loaded) ? $loaded : [];
}
return isset($zhMap[$title]) && is_string($zhMap[$title]) ? $zhMap[$title] : $title;
}
private function menuTitleToEn(string $title): string
{
static $enMap = null;
if (!is_array($enMap)) {
$mapFile = app_path() . '/common/lang/en/admin_rule_title.php';
$loaded = is_file($mapFile) ? include $mapFile : [];
$enMap = is_array($loaded) ? $loaded : [];
}
return isset($enMap[$title]) && is_string($enMap[$title]) ? $enMap[$title] : $title;
}
} }

View File

@@ -208,6 +208,7 @@ Route::post('/admin/auth/group/add', [\app\admin\controller\auth\Group::class, '
Route::post('/admin/auth/group/edit', [\app\admin\controller\auth\Group::class, 'edit']); Route::post('/admin/auth/group/edit', [\app\admin\controller\auth\Group::class, 'edit']);
Route::post('/admin/auth/group/del', [\app\admin\controller\auth\Group::class, 'del']); Route::post('/admin/auth/group/del', [\app\admin\controller\auth\Group::class, 'del']);
Route::get('/admin/auth/group/select', [\app\admin\controller\auth\Group::class, 'select']); Route::get('/admin/auth/group/select', [\app\admin\controller\auth\Group::class, 'select']);
Route::get('/admin/auth/group/rules', [\app\admin\controller\auth\Group::class, 'rules']);
// admin/auth/rule // admin/auth/rule
Route::get('/admin/auth/rule/index', [\app\admin\controller\auth\Rule::class, 'index']); Route::get('/admin/auth/rule/index', [\app\admin\controller\auth\Rule::class, 'index']);

View File

@@ -2,7 +2,7 @@ import createAxios from '/@/utils/axios'
export function getAdminRules() { export function getAdminRules() {
return createAxios({ return createAxios({
url: '/admin/auth.Rule/index', url: '/admin/auth.Group/rules',
method: 'get', method: 'get',
params: { params: {
force_menu_zh: 1, force_menu_zh: 1,

View File

@@ -104,6 +104,14 @@ const baTable = new baTableClass(
} }
) )
// 编辑自身时不提交角色组,避免与后端「不可修改自己所在管理组」校验冲突
baTable.before.onSubmit = ({ operate, items }) => {
if (operate === 'edit' && items.id == adminInfo.id) {
delete items.group_arr
delete items.group_name_arr
}
}
provide('baTable', baTable) provide('baTable', baTable)
baTable.mount() baTable.mount()

View File

@@ -162,6 +162,9 @@ const rules: Partial<Record<string, FormItemRule[]>> = reactive({
{ {
required: true, required: true,
validator: (_rule: any, val: unknown, callback: Function) => { validator: (_rule: any, val: unknown, callback: Function) => {
if (baTable.form.operate === 'Edit' && adminInfo.id == baTable.form.items?.id) {
return callback()
}
if (Array.isArray(val)) { if (Array.isArray(val)) {
if (val.length !== 1) { if (val.length !== 1) {
return callback(new Error(t('auth.admin.Please select exactly one group'))) return callback(new Error(t('auth.admin.Please select exactly one group')))

View File

@@ -182,9 +182,17 @@ const menuRuleTreeUpdate = () => {
if (baTable.form.items!.rules && baTable.form.items!.rules.length) { if (baTable.form.items!.rules && baTable.form.items!.rules.length) {
if (baTable.form.items!.rules.includes('*')) { if (baTable.form.items!.rules.includes('*')) {
let arr: number[] = [] let arr: number[] = []
for (const key in baTable.form.extend!.menuRules) { const walk = (nodes: anyObj[]) => {
arr.push(baTable.form.extend!.menuRules[key].id) nodes.forEach((node) => {
if (node.id) {
arr.push(node.id)
}
if (node.children?.length) {
walk(node.children)
}
})
} }
walk(res.data.list || [])
baTable.form.extend!.defaultCheckedKeys = arr baTable.form.extend!.defaultCheckedKeys = arr
} else { } else {
baTable.form.extend!.defaultCheckedKeys = baTable.form.items!.rules baTable.form.extend!.defaultCheckedKeys = baTable.form.items!.rules