用户管理布局与管理员身份权限设置 (#119)
This commit is contained in:
parent
8dc9f3bc6f
commit
e94d556125
@ -13,7 +13,7 @@ class Navigation extends Controller
|
||||
parent::__construct();
|
||||
|
||||
$this->middleware(function ($request, $next) {
|
||||
abort_unless(user()?->hasRole('admin'), 403);
|
||||
abort_unless(\App\Utilities\SystemAccess::canOpenSettings(), 403);
|
||||
|
||||
return $next($request);
|
||||
});
|
||||
|
||||
@ -21,6 +21,11 @@ class Sidebar extends Controller
|
||||
});
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
return view('system_settings.sidebar.index');
|
||||
}
|
||||
|
||||
public function edit(Request $request)
|
||||
{
|
||||
$subjects = $this->subjects();
|
||||
@ -63,7 +68,7 @@ class Sidebar extends Controller
|
||||
|
||||
flash('保存成功,侧边栏权限已更新。')->success();
|
||||
|
||||
return redirect()->route('dashboard');
|
||||
return redirect()->route('system-settings.sidebar.edit', ['subject' => $subject]);
|
||||
}
|
||||
|
||||
private function subjects(): array
|
||||
@ -72,7 +77,6 @@ class Sidebar extends Controller
|
||||
|
||||
if (SystemAccess::isSuperAdmin()) {
|
||||
$subjects = [
|
||||
'role_super_admin' => '用户类型:' . $labels[SystemAccess::SUPER_ADMIN],
|
||||
'role_admin' => '用户类型:' . $labels[SystemAccess::ADMIN],
|
||||
'role_user' => '用户类型:' . $labels[SystemAccess::USER],
|
||||
];
|
||||
@ -94,6 +98,9 @@ class Sidebar extends Controller
|
||||
|
||||
foreach ($users as $managedUser) {
|
||||
$role = SystemAccess::roleName($managedUser);
|
||||
if ($role === SystemAccess::SUPER_ADMIN) {
|
||||
continue;
|
||||
}
|
||||
$subjects['user_' . $managedUser->id] = '指定用户:' . $managedUser->name . ' - ' . ($labels[$role] ?? $role);
|
||||
}
|
||||
|
||||
|
||||
@ -87,12 +87,14 @@ class Users extends Controller
|
||||
$role = Role::where('name', $data['role'])->firstOrFail();
|
||||
|
||||
if (
|
||||
$managedUser->id === user()->id
|
||||
&& SystemAccess::isSuperAdmin($managedUser)
|
||||
&& $data['role'] !== SystemAccess::SUPER_ADMIN
|
||||
SystemAccess::isSuperAdmin($managedUser)
|
||||
&& $managedUser->enabled
|
||||
&& ($data['role'] !== SystemAccess::SUPER_ADMIN || ! ($data['enabled'] ?? false))
|
||||
&& $this->superAdminCount() <= 1
|
||||
) {
|
||||
return back()->withErrors(['role' => '系统至少需要保留一名超级管理员。'])->withInput();
|
||||
return back()->withErrors([
|
||||
$data['role'] !== SystemAccess::SUPER_ADMIN ? 'role' : 'enabled' => '系统至少需要保留一名启用的主管理员。',
|
||||
])->withInput();
|
||||
}
|
||||
|
||||
if ($managedUser->id === user()->id && ! ($data['enabled'] ?? false)) {
|
||||
@ -111,9 +113,11 @@ class Users extends Controller
|
||||
}
|
||||
|
||||
$managedUser->update($attributes);
|
||||
$managedUser->roles()->sync([$role->id]);
|
||||
$managedUser->syncRoles([$role->id]);
|
||||
});
|
||||
|
||||
$managedUser->flushCache();
|
||||
|
||||
flash('用户已修改。')->success();
|
||||
|
||||
return redirect()->route('system-settings.users.index');
|
||||
@ -126,8 +130,8 @@ class Users extends Controller
|
||||
abort_if($managedUser->id === user()->id, 422, '不能删除当前登录账号。');
|
||||
abort_unless(SystemAccess::canManageUser(user(), $managedUser), 403);
|
||||
|
||||
if (SystemAccess::isSuperAdmin($managedUser) && $this->superAdminCount() <= 1) {
|
||||
return back()->withErrors(['user' => '系统至少需要保留一名超级管理员。']);
|
||||
if (SystemAccess::isSuperAdmin($managedUser) && $managedUser->enabled && $this->superAdminCount() <= 1) {
|
||||
return back()->withErrors(['user' => '系统至少需要保留一名启用的主管理员。']);
|
||||
}
|
||||
|
||||
$managedUser->delete();
|
||||
@ -143,10 +147,6 @@ class Users extends Controller
|
||||
$company->where('companies.id', company_id());
|
||||
});
|
||||
|
||||
if (! SystemAccess::isSuperAdmin()) {
|
||||
$query->whereHas('roles', fn ($roles) => $roles->where('roles.name', SystemAccess::USER));
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
@ -176,6 +176,7 @@ class Users extends Controller
|
||||
private function superAdminCount(): int
|
||||
{
|
||||
return User::query()
|
||||
->where('enabled', true)
|
||||
->whereHas('roles', fn ($roles) => $roles->where('roles.name', SystemAccess::SUPER_ADMIN))
|
||||
->count();
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ class Settings extends Component
|
||||
|
||||
$items = [
|
||||
['system-settings.users.index', '用户管理', 'people', '用户 类型 权限'],
|
||||
['system-settings.sidebar.edit', '侧边栏管理', 'menu', '侧边栏 用户类型 指定管理员'],
|
||||
['system-settings.permissions.index', '权限管理', 'admin_panel_settings', '侧边栏 用户页面 访问权限 管理员 身份'],
|
||||
['system-settings.databases.index', '数据库管理', 'storage', '系统数据库 云数据库 上游 连接 测试'],
|
||||
];
|
||||
|
||||
@ -38,6 +38,8 @@ class Settings extends Component
|
||||
$item = $menu->route($route, $title, [], ($index + 1) * 10, [
|
||||
'icon' => $icon,
|
||||
'search_keywords' => $keywords,
|
||||
'active' => fn () => request()->routeIs(Str::beforeLast($route, '.') . '.*')
|
||||
|| ($route === 'system-settings.permissions.index' && request()->routeIs('system-settings.sidebar.*')),
|
||||
]);
|
||||
|
||||
if ($item->isActive()) {
|
||||
|
||||
@ -14,6 +14,30 @@ class User extends FormRequest
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
$actor = $this->user();
|
||||
if ($actor && ! \App\Utilities\SystemAccess::isSuperAdmin($actor)) {
|
||||
$target = $this->route('user');
|
||||
$target = $target instanceof \App\Models\Auth\User ? $target : \App\Models\Auth\User::find($target);
|
||||
if ($target && $target->id !== $actor->id && \App\Utilities\SystemAccess::canOpenSettings($target)) {
|
||||
return false;
|
||||
}
|
||||
if ($this->filled('permissions')) {
|
||||
return false;
|
||||
}
|
||||
if ($this->has('roles')) {
|
||||
$requested = array_map('strval', (array) $this->input('roles'));
|
||||
$current = $target ? $target->roles()->pluck('id')->map(fn ($id) => (string) $id)->all() : [];
|
||||
sort($requested);
|
||||
sort($current);
|
||||
if ($requested !== $current) {
|
||||
$allowed = \App\Models\Auth\Role::where('name', 'user')->pluck('id')->map(fn ($id) => (string) $id)->all();
|
||||
if (array_diff($requested, $allowed)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ class ShowInSettings
|
||||
{
|
||||
$menu = $event->menu;
|
||||
|
||||
if (user()?->hasRole('admin')) {
|
||||
if (\App\Utilities\SystemAccess::canOpenSettings()) {
|
||||
$menu->route('settings.navigation.manage', '侧边栏功能名称', [], 75, [
|
||||
'icon' => 'menu',
|
||||
'search_keywords' => '侧边栏 功能 名称',
|
||||
|
||||
@ -31,6 +31,10 @@ class SidebarPermissions
|
||||
|
||||
public static function allowedKeys(User $user): ?array
|
||||
{
|
||||
if (SystemAccess::isSuperAdmin($user)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$settings = self::settings();
|
||||
$subject = self::subjectKey($user);
|
||||
|
||||
@ -57,7 +61,7 @@ class SidebarPermissions
|
||||
return $section;
|
||||
}
|
||||
|
||||
$pages = array_values(array_filter($section['pages'] ?? [], function (array $page) use ($allowed, $sectionKey) {
|
||||
$pages = array_values(array_filter(array_map(function (array $page) use ($allowed, $sectionKey) {
|
||||
$pageKey = $sectionKey . '/' . $page['slug'];
|
||||
|
||||
if (in_array($pageKey, $allowed, true)) {
|
||||
@ -67,7 +71,7 @@ class SidebarPermissions
|
||||
return in_array($sectionKey . '/' . $child['slug'], $allowed, true);
|
||||
}));
|
||||
}
|
||||
return true;
|
||||
return $page;
|
||||
}
|
||||
|
||||
// 子页面可能在 allowed 列表中(独立 URL),父级分组也允许展开
|
||||
@ -77,12 +81,12 @@ class SidebarPermissions
|
||||
}));
|
||||
if (! empty($visibleChildren)) {
|
||||
$page['children'] = $visibleChildren;
|
||||
return true;
|
||||
return $page;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}));
|
||||
return null;
|
||||
}, $section['pages'] ?? [])));
|
||||
|
||||
if (! empty($pages)) {
|
||||
$section['pages'] = $pages;
|
||||
|
||||
@ -18,8 +18,11 @@ class SystemAccess
|
||||
return '';
|
||||
}
|
||||
|
||||
// 身份变更必须立即生效,不能沿用权限库中已缓存的旧角色。
|
||||
$roles = $user->roles()->pluck('name')->all();
|
||||
|
||||
foreach ([self::SUPER_ADMIN, self::ADMIN, self::USER] as $role) {
|
||||
if ($user->hasRole($role)) {
|
||||
if (in_array($role, $roles, true)) {
|
||||
return $role;
|
||||
}
|
||||
}
|
||||
@ -53,8 +56,8 @@ class SystemAccess
|
||||
|
||||
if (self::isSuperAdmin($actor)) {
|
||||
return [
|
||||
self::SUPER_ADMIN => '超级管理员',
|
||||
self::ADMIN => '普通管理员',
|
||||
self::SUPER_ADMIN => '主管理员(最高权限)',
|
||||
self::ADMIN => '管理员',
|
||||
self::USER => '普通用户',
|
||||
];
|
||||
}
|
||||
@ -65,8 +68,8 @@ class SystemAccess
|
||||
public static function roleLabels(): array
|
||||
{
|
||||
return [
|
||||
self::SUPER_ADMIN => '超级管理员',
|
||||
self::ADMIN => '普通管理员',
|
||||
self::SUPER_ADMIN => '主管理员',
|
||||
self::ADMIN => '管理员',
|
||||
self::USER => '普通用户',
|
||||
];
|
||||
}
|
||||
|
||||
@ -0,0 +1,74 @@
|
||||
@once
|
||||
@push('scripts_start')
|
||||
<script>
|
||||
(() => {
|
||||
if (window.caiwuSettingsNavigation) return;
|
||||
window.caiwuSettingsNavigation = true;
|
||||
let pending;
|
||||
const supports = url => url.origin === location.origin
|
||||
&& /\/system-settings\/(?:users(?:\/create|\/\d+\/edit)?|permissions|sidebar)$/.test(url.pathname);
|
||||
async function navigate(url, push = true) {
|
||||
const current = document.querySelector('[data-settings-workspace]');
|
||||
if (!current) return;
|
||||
pending?.abort();
|
||||
const controller = new AbortController();
|
||||
pending = controller;
|
||||
current.setAttribute('aria-busy', 'true');
|
||||
try {
|
||||
const response = await fetch(url, {signal:controller.signal, credentials:'same-origin'});
|
||||
const page = new DOMParser().parseFromString(await response.text(), 'text/html');
|
||||
const next = page.querySelector('[data-settings-workspace]');
|
||||
const header = page.querySelector('#header');
|
||||
if (!response.ok || !next || !header) throw new Error('unavailable');
|
||||
current.replaceWith(next);
|
||||
document.querySelector('#header')?.replaceWith(header);
|
||||
document.title = page.title;
|
||||
document.querySelectorAll('#menu-settings a[id]').forEach(link => {
|
||||
const updated = page.getElementById(link.id);
|
||||
if (updated) link.className = updated.className;
|
||||
});
|
||||
if (push) {
|
||||
history.replaceState({...history.state, settingsPage:true}, '', location.href);
|
||||
history.pushState({settingsPage:true}, '', url);
|
||||
}
|
||||
const title = header.querySelector('h1');
|
||||
title?.setAttribute('tabindex', '-1');
|
||||
title?.focus({preventScroll:true});
|
||||
window.scrollTo(0, 0);
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') return;
|
||||
let alert = current.querySelector('[data-settings-navigation-error]');
|
||||
if (!alert) {
|
||||
alert = document.createElement('p');
|
||||
alert.dataset.settingsNavigationError = '';
|
||||
alert.setAttribute('role', 'alert');
|
||||
alert.className = 'mb-4 text-sm text-red-700';
|
||||
current.prepend(alert);
|
||||
}
|
||||
alert.textContent = '页面暂时无法加载,请重试。';
|
||||
} finally {
|
||||
current.removeAttribute('aria-busy');
|
||||
}
|
||||
}
|
||||
document.addEventListener('click', event => {
|
||||
const link = event.target.closest('a[href]');
|
||||
if (!link || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || link.target) return;
|
||||
const url = new URL(link.href);
|
||||
if (!document.querySelector('[data-settings-workspace]') || !supports(url)) return;
|
||||
event.preventDefault();
|
||||
navigate(url.href);
|
||||
});
|
||||
document.addEventListener('change', event => {
|
||||
if (!event.target.matches('[data-settings-subject]')) return;
|
||||
const form = event.target.form;
|
||||
const url = new URL(form.action);
|
||||
url.search = new URLSearchParams(new FormData(form)).toString();
|
||||
navigate(url.href);
|
||||
});
|
||||
window.addEventListener('popstate', event => {
|
||||
if (event.state?.settingsPage && supports(new URL(location.href))) navigate(location.href, false);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
@ -1,21 +1,23 @@
|
||||
<nav class="mb-6 flex flex-wrap items-center gap-3 rounded-xl border border-gray-200 bg-white p-3 shadow-sm" aria-label="系统设置">
|
||||
@include('system_settings.partials.styles')
|
||||
@include('system_settings.partials.navigation-script')
|
||||
<nav class="mb-6 flex flex-wrap items-center gap-3 border-b border-gray-200 bg-white p-3" aria-label="系统设置">
|
||||
@php
|
||||
$items = [
|
||||
['system-settings.users.index', '用户管理', 'people'],
|
||||
['system-settings.dingtalk-bindings.index', '钉钉绑定', 'admin_panel_settings'],
|
||||
['system-settings.sidebar.edit', '侧边栏管理', 'menu'],
|
||||
['system-settings.permissions.index', '权限管理', 'admin_panel_settings'],
|
||||
['system-settings.databases.index', '数据库管理', 'storage'],
|
||||
];
|
||||
@endphp
|
||||
|
||||
@foreach ($items as [$routeName, $label, $icon])
|
||||
@php $active = request()->routeIs(Str::beforeLast($routeName, '.') . '.*'); @endphp
|
||||
@php $active = request()->routeIs(Str::beforeLast($routeName, '.') . '.*') || ($routeName === 'system-settings.permissions.index' && request()->routeIs('system-settings.sidebar.*')); @endphp
|
||||
<a
|
||||
href="{{ route($routeName) }}"
|
||||
@class([
|
||||
'inline-flex items-center gap-2 rounded-lg px-4 py-2.5 text-sm font-semibold transition-colors',
|
||||
'bg-purple-700 text-white' => $active,
|
||||
'text-gray-600 hover:bg-purple-100 hover:text-purple-700' => ! $active,
|
||||
'settings-nav-active' => $active,
|
||||
'text-gray-600 settings-nav-hover' => ! $active,
|
||||
])
|
||||
@if ($active) aria-current="page" @endif
|
||||
>
|
||||
|
||||
15
resources/views/system_settings/partials/styles.blade.php
Normal file
15
resources/views/system_settings/partials/styles.blade.php
Normal file
@ -0,0 +1,15 @@
|
||||
<style>
|
||||
#main-body > .container { max-width: none; width: auto; margin-left: 1.5rem; margin-right: 1.5rem; }
|
||||
.settings-primary { display:inline-flex; align-items:center; justify-content:center; gap:8px; min-height:34px; padding:0 14px; border-radius:8px; background:var(--vip-content-accent-dark, #856632); color:#fff; font-size:13px; }
|
||||
.settings-primary:hover { background:#6A5129; color:#fff; }
|
||||
.settings-accent { color:var(--vip-content-accent-dark, #856632); }
|
||||
.settings-nav-active { background:#F7F0DE; color:#856632; }
|
||||
.settings-nav-hover:hover { background:#F7F0DE; color:#856632; }
|
||||
.settings-field-focus:focus, .settings-primary:focus-visible { outline:2px solid #856632; outline-offset:2px; }
|
||||
.settings-users-table { width:100%; min-width:680px; table-layout:fixed; }
|
||||
.settings-users-table th { white-space:nowrap; }
|
||||
.settings-users-table td { overflow-wrap:anywhere; }
|
||||
.settings-users-table .settings-email { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
@media(min-width:1280px) { #main-body > .container { margin-left:2.5rem; margin-right:2.5rem; } }
|
||||
@media(max-width:767px) { #main-body > .container { margin-left:1rem; margin-right:1rem; } }
|
||||
</style>
|
||||
@ -1,18 +1,19 @@
|
||||
<x-layouts.admin>
|
||||
<x-slot name="title">侧边栏管理</x-slot>
|
||||
<x-slot name="title">用户可访问页面</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="my-6">
|
||||
<div class="my-6" data-settings-workspace>
|
||||
@include('system_settings.partials.navigation')
|
||||
<nav class="mb-4 text-sm" aria-label="页面位置"><a class="settings-accent" href="{{ route('system-settings.permissions.index') }}">权限管理</a><span class="mx-2" aria-hidden="true">/</span><span aria-current="page">用户可访问页面</span></nav>
|
||||
|
||||
<div class="mb-5 rounded-lg border-l-4 border-purple bg-lilac-100 px-4 py-3 text-sm text-gray-700">
|
||||
先选择用户类型或指定用户,再勾选允许显示的一级、二级目录。指定用户配置优先于用户类型配置。
|
||||
<div class="mb-5 rounded-lg border border-gray-200 bg-white px-4 py-3 text-sm text-gray-700">
|
||||
先选择权限身份或指定用户,再勾选可访问页面。指定用户配置优先于角色配置;主管理员拥有全部页面权限。
|
||||
</div>
|
||||
|
||||
<form method="GET" action="{{ route('system-settings.sidebar.edit') }}" class="mb-5 rounded-xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
<form method="GET" action="{{ route('system-settings.sidebar.edit') }}" class="mb-5 rounded-xl border border-gray-200 bg-white p-5">
|
||||
<label class="block max-w-xl">
|
||||
<span class="text-sm font-semibold text-gray-700">权限对象</span>
|
||||
<select name="subject" onchange="this.form.submit()" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2.5 text-sm focus:border-purple focus:ring-purple">
|
||||
<select name="subject" data-settings-subject class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2.5 text-sm settings-field-focus">
|
||||
@foreach ($subjects as $key => $label)
|
||||
<option value="{{ $key }}" @selected($subject === $key)>{{ $label }}</option>
|
||||
@endforeach
|
||||
@ -20,7 +21,7 @@
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ route('system-settings.sidebar.update') }}" class="rounded-xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
<form method="POST" action="{{ route('system-settings.sidebar.update') }}" class="rounded-xl border border-gray-200 bg-white p-5">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
<input type="hidden" name="subject" value="{{ $subject }}">
|
||||
@ -33,7 +34,7 @@
|
||||
|
||||
<div class="mb-5 flex flex-wrap items-center justify-between gap-3 border-b border-gray-100 pb-4">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-800">{{ $subjects[$subject] }}</h2>
|
||||
<h2 class="text-sm font-semibold text-gray-800">{{ $subjects[$subject] }}</h2>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ $configured ? '当前正在使用自定义权限。' : '当前未单独配置,默认显示全部目录。保存后将启用自定义权限。' }}</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@ -42,32 +43,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<div class="grid grid-cols-1 items-start gap-4 lg:grid-cols-2">
|
||||
@foreach ($navigation as $section)
|
||||
<details class="group rounded-xl border border-gray-200 bg-white">
|
||||
<summary class="flex cursor-pointer list-none items-center gap-3 p-4 font-semibold text-gray-800">
|
||||
<span class="material-icons-outlined text-xl text-purple">{{ $section['icon'] }}</span>
|
||||
<span class="material-icons-outlined text-xl settings-accent">{{ $section['icon'] }}</span>
|
||||
<span>{{ $section['title'] }}</span>
|
||||
<span class="ml-auto text-xs font-normal text-gray-400">点击展开二级目录</span>
|
||||
<span class="material-icons-outlined text-gray-400 transition-transform group-open:rotate-180">expand_more</span>
|
||||
</summary>
|
||||
|
||||
<div class="border-t border-gray-100 p-4">
|
||||
<label class="mb-3 flex items-center gap-2 rounded-lg bg-lilac-100 px-3 py-2 text-sm font-semibold text-purple">
|
||||
<label class="mb-3 flex items-center gap-2 rounded-lg bg-lilac-100 px-3 py-2 text-sm font-semibold settings-accent">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="permissions[]"
|
||||
value="{{ $section['slug'] }}"
|
||||
data-sidebar-permission
|
||||
@checked(in_array($section['slug'], $selected, true))
|
||||
class="rounded border-gray-300 text-purple focus:ring-purple"
|
||||
class="rounded border-gray-300 settings-accent settings-field-focus"
|
||||
>
|
||||
允许整个一级目录
|
||||
</label>
|
||||
|
||||
@if (! empty($section['pages']))
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
@foreach ($section['pages'] as $page)
|
||||
@foreach (collect($section['pages'])->flatMap(fn ($entry) => empty($entry['children']) ? [$entry] : $entry['children']) as $page)
|
||||
@php $pageKey = $section['slug'] . '/' . $page['slug']; @endphp
|
||||
<label class="flex items-center gap-2 rounded-lg px-2 py-1.5 text-sm text-gray-600 hover:bg-gray-50">
|
||||
<input
|
||||
@ -76,7 +77,7 @@
|
||||
value="{{ $pageKey }}"
|
||||
data-sidebar-permission
|
||||
@checked(in_array($pageKey, $selected, true))
|
||||
class="rounded border-gray-300 text-purple focus:ring-purple"
|
||||
class="rounded border-gray-300 settings-accent settings-field-focus"
|
||||
>
|
||||
{{ $page['title'] }}
|
||||
</label>
|
||||
@ -91,7 +92,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end border-t border-gray-100 pt-5">
|
||||
<button type="submit" class="rounded-lg bg-green px-5 py-2.5 text-sm text-white hover:bg-green-700">保存权限</button>
|
||||
<button type="submit" class="rounded-lg settings-primary">保存权限</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
19
resources/views/system_settings/sidebar/index.blade.php
Normal file
19
resources/views/system_settings/sidebar/index.blade.php
Normal file
@ -0,0 +1,19 @@
|
||||
<x-layouts.admin>
|
||||
<x-slot name="title">权限管理</x-slot>
|
||||
<x-slot name="content">
|
||||
<div class="my-6" data-settings-workspace>
|
||||
@include('system_settings.partials.navigation')
|
||||
<div class="overflow-x-auto rounded-xl border border-gray-200 bg-white">
|
||||
<table class="min-w-full text-sm text-left">
|
||||
<thead class="bg-gray-50"><tr><th class="px-5 py-3">设置项</th><th class="px-5 py-3">作用</th><th class="px-5 py-3 text-right">操作</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="px-5 py-4 whitespace-nowrap">用户可访问页面</td><td class="px-5 py-4">设置用户或角色可以访问的业务页面。主管理员始终拥有全部页面权限。</td><td class="px-5 py-4 text-right whitespace-nowrap"><a class="settings-accent" href="{{ route('system-settings.sidebar.edit') }}">设置页面权限</a></td></tr>
|
||||
@if (\App\Utilities\SystemAccess::isSuperAdmin())
|
||||
<tr class="border-t border-gray-200"><td class="px-5 py-4 whitespace-nowrap">用户权限身份</td><td class="px-5 py-4">在用户管理中设置普通用户、管理员或主管理员身份。</td><td class="px-5 py-4 text-right whitespace-nowrap"><a class="settings-accent" href="{{ route('system-settings.users.index') }}">管理用户身份</a></td></tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-layouts.admin>
|
||||
@ -2,10 +2,10 @@
|
||||
<x-slot name="title">{{ $managedUser ? '修改用户' : '新增用户' }}</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="my-6">
|
||||
<div class="my-6" data-settings-workspace>
|
||||
@include('system_settings.partials.navigation')
|
||||
|
||||
<form method="POST" action="{{ $managedUser ? route('system-settings.users.update', $managedUser->id) : route('system-settings.users.store') }}" class="max-w-3xl rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<form method="POST" action="{{ $managedUser ? route('system-settings.users.update', $managedUser->id) : route('system-settings.users.store') }}" class="w-full rounded-xl border border-gray-200 bg-white p-6">
|
||||
@csrf
|
||||
@if ($managedUser) @method('PATCH') @endif
|
||||
|
||||
@ -18,41 +18,44 @@
|
||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2">
|
||||
<label class="block">
|
||||
<span class="text-sm font-semibold text-gray-700">姓名 *</span>
|
||||
<input name="name" value="{{ old('name', $managedUser?->name) }}" required maxlength="191" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2.5 text-sm focus:border-purple focus:ring-purple">
|
||||
<input name="name" value="{{ old('name', $managedUser?->name) }}" required maxlength="191" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2.5 text-sm settings-field-focus">
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm font-semibold text-gray-700">邮箱 *</span>
|
||||
<input type="email" name="email" value="{{ old('email', $managedUser?->email) }}" required maxlength="191" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2.5 text-sm focus:border-purple focus:ring-purple">
|
||||
<input type="email" name="email" value="{{ old('email', $managedUser?->email) }}" required maxlength="191" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2.5 text-sm settings-field-focus">
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm font-semibold text-gray-700">用户类型 *</span>
|
||||
<select name="role" required class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2.5 text-sm focus:border-purple focus:ring-purple">
|
||||
<span class="text-sm font-semibold text-gray-700">权限身份 *</span>
|
||||
<select name="role" required class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2.5 text-sm settings-field-focus">
|
||||
@foreach ($roles as $role => $label)
|
||||
<option value="{{ $role }}" @selected(old('role', $managedUser ? \App\Utilities\SystemAccess::roleName($managedUser) : \App\Utilities\SystemAccess::USER) === $role)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if (\App\Utilities\SystemAccess::isSuperAdmin())
|
||||
<span class="mt-1 block text-xs text-gray-600">主管理员拥有最高权限,可管理全部用户,且不受页面访问范围限制。</span>
|
||||
@endif
|
||||
</label>
|
||||
<div class="flex items-end pb-2">
|
||||
<input type="hidden" name="enabled" value="0">
|
||||
<label class="inline-flex items-center gap-2 text-sm font-semibold text-gray-700">
|
||||
<input type="checkbox" name="enabled" value="1" @checked((bool) old('enabled', $managedUser?->enabled ?? true)) class="rounded border-gray-300 text-purple focus:ring-purple">
|
||||
<input type="checkbox" name="enabled" value="1" @checked((bool) old('enabled', $managedUser?->enabled ?? true)) class="rounded border-gray-300 settings-accent settings-field-focus">
|
||||
启用账号
|
||||
</label>
|
||||
</div>
|
||||
<label class="block">
|
||||
<span class="text-sm font-semibold text-gray-700">密码 {{ $managedUser ? '' : '*' }}</span>
|
||||
<input type="password" name="password" @if (! $managedUser) required @endif minlength="8" autocomplete="new-password" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2.5 text-sm focus:border-purple focus:ring-purple">
|
||||
<input type="password" name="password" @if (! $managedUser) required @endif minlength="8" autocomplete="new-password" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2.5 text-sm settings-field-focus">
|
||||
@if ($managedUser)<span class="mt-1 block text-xs text-gray-400">留空表示不修改密码</span>@endif
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm font-semibold text-gray-700">确认密码 {{ $managedUser ? '' : '*' }}</span>
|
||||
<input type="password" name="password_confirmation" @if (! $managedUser) required @endif minlength="8" autocomplete="new-password" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2.5 text-sm focus:border-purple focus:ring-purple">
|
||||
<input type="password" name="password_confirmation" @if (! $managedUser) required @endif minlength="8" autocomplete="new-password" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2.5 text-sm settings-field-focus">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mt-7 flex justify-end gap-3 border-t border-gray-100 pt-5">
|
||||
<a href="{{ route('system-settings.users.index') }}" class="rounded-lg px-5 py-2.5 text-sm text-gray-600 hover:bg-gray-100">取消</a>
|
||||
<button type="submit" class="rounded-lg bg-green px-5 py-2.5 text-sm text-white hover:bg-green-700">保存</button>
|
||||
<button type="submit" class="rounded-lg settings-primary">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -2,21 +2,21 @@
|
||||
<x-slot name="title">用户管理</x-slot>
|
||||
|
||||
<x-slot name="buttons">
|
||||
<a href="{{ route('system-settings.users.create') }}" class="inline-flex items-center gap-2 rounded-lg bg-green px-5 py-2 text-sm text-white hover:bg-green-700">
|
||||
<a href="{{ route('system-settings.users.create') }}" class="inline-flex items-center gap-2 rounded-lg settings-primary">
|
||||
<span class="material-icons-outlined text-lg">person_add</span>
|
||||
新增用户
|
||||
</a>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="my-6">
|
||||
<div class="my-6" data-settings-workspace>
|
||||
@include('system_settings.partials.navigation')
|
||||
|
||||
<div class="mb-4 rounded-lg border-l-4 border-purple bg-lilac-100 px-4 py-3 text-sm text-gray-700">
|
||||
<div class="mb-4 rounded-lg border border-gray-200 bg-white px-4 py-3 text-sm text-gray-700">
|
||||
@if (\App\Utilities\SystemAccess::isSuperAdmin())
|
||||
超级管理员可管理所有用户和用户类型。
|
||||
主管理员拥有最高权限,可管理全部用户、设置管理员身份和页面访问范围。
|
||||
@else
|
||||
普通管理员只能新增、修改和删除普通用户。
|
||||
已显示当前公司的全部账号。管理员可管理普通用户;管理员身份由主管理员设置。
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@ -27,12 +27,13 @@
|
||||
@endif
|
||||
|
||||
<div class="overflow-x-auto rounded-xl border border-gray-200 bg-white">
|
||||
<table class="min-w-full divide-y divide-gray-200 text-sm">
|
||||
<table class="settings-users-table divide-y divide-gray-200 text-sm">
|
||||
<colgroup><col style="width:18%"><col style="width:32%"><col style="width:16%"><col style="width:10%"><col style="width:24%"></colgroup>
|
||||
<thead class="bg-gray-50 text-left text-gray-500">
|
||||
<tr>
|
||||
<th class="px-5 py-3">姓名</th>
|
||||
<th class="px-5 py-3">邮箱</th>
|
||||
<th class="px-5 py-3">用户类型</th>
|
||||
<th class="px-5 py-3">权限身份</th>
|
||||
<th class="px-5 py-3">状态</th>
|
||||
<th class="px-5 py-3 text-right">操作</th>
|
||||
</tr>
|
||||
@ -44,10 +45,10 @@
|
||||
<td class="px-5 py-4 font-semibold text-gray-800">
|
||||
{{ $managedUser->name }}
|
||||
@if ($managedUser->id === user()->id)
|
||||
<span class="ml-2 rounded-full bg-lilac-100 px-2 py-0.5 text-xs text-purple">当前账号</span>
|
||||
<span class="ml-2 inline-block whitespace-nowrap rounded-full bg-lilac-100 px-2 py-0.5 text-xs settings-accent">当前账号</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-5 py-4 text-gray-600">{{ $managedUser->email }}</td>
|
||||
<td class="px-5 py-4 text-gray-600"><span class="settings-email" title="{{ $managedUser->email }}">{{ $managedUser->email }}</span></td>
|
||||
<td class="px-5 py-4 text-gray-600">{{ $roleLabels[$roleName] ?? ($managedUser->roles->first()?->display_name ?? '-') }}</td>
|
||||
<td class="px-5 py-4">
|
||||
<span @class([
|
||||
@ -58,7 +59,10 @@
|
||||
</td>
|
||||
<td class="px-5 py-4 text-right whitespace-nowrap">
|
||||
@if (\App\Utilities\SystemAccess::canManageUser(user(), $managedUser))
|
||||
<a href="{{ route('system-settings.users.edit', $managedUser->id) }}" class="mr-4 text-purple hover:underline">修改</a>
|
||||
<a href="{{ route('system-settings.users.edit', $managedUser->id) }}" class="mr-4 settings-accent hover:underline">修改</a>
|
||||
@if (in_array($roleName, [\App\Utilities\SystemAccess::ADMIN, \App\Utilities\SystemAccess::USER], true))
|
||||
<a href="{{ route('system-settings.sidebar.edit', ['subject' => 'user_'.$managedUser->id]) }}" class="mr-4 settings-accent hover:underline">页面权限</a>
|
||||
@endif
|
||||
@if ($managedUser->id !== user()->id)
|
||||
<form method="POST" action="{{ route('system-settings.users.destroy', $managedUser->id) }}" class="inline" onsubmit="return confirm('确定删除该用户吗?')">
|
||||
@csrf
|
||||
|
||||
@ -179,6 +179,7 @@ Route::group(['as' => 'system-settings.', 'prefix' => 'system-settings'], functi
|
||||
Route::post('dingtalk-bindings/{bindingRequest}/reject', 'SystemSettings\DingTalkBindings@reject')->name('dingtalk-bindings.reject');
|
||||
Route::delete('dingtalk-bindings/identities/{identity}', 'SystemSettings\DingTalkBindings@destroy')->name('dingtalk-bindings.destroy');
|
||||
|
||||
Route::get('permissions', 'SystemSettings\Sidebar@index')->name('permissions.index');
|
||||
Route::get('sidebar', 'SystemSettings\Sidebar@edit')->name('sidebar.edit');
|
||||
Route::patch('sidebar', 'SystemSettings\Sidebar@update')->name('sidebar.update');
|
||||
|
||||
|
||||
@ -442,7 +442,7 @@ class DingTalkLoginTest extends FeatureTestCase
|
||||
->assertOk()->assertSee('value="user_'.$created->id.'"', false);
|
||||
$this->patch(route('system-settings.sidebar.update'), [
|
||||
'subject' => 'user_'.$created->id, 'permissions' => ['store-management/basic-information'],
|
||||
])->assertRedirect(route('dashboard'));
|
||||
])->assertRedirect(route('system-settings.sidebar.edit', ['subject' => 'user_'.$created->id]));
|
||||
$this->assertSame(['store-management/basic-information'], SidebarPermissions::allowedKeys($created));
|
||||
$this->actingAs($created)->withSession(['dingtalk_identity_id' => $created->dingtalkIdentity->id, 'dingtalk_roster_checked_at' => now()->timestamp])
|
||||
->get(route('dashboard'))->assertRedirectContains('/workspace/store-management/basic-information');
|
||||
@ -453,6 +453,7 @@ class DingTalkLoginTest extends FeatureTestCase
|
||||
|
||||
public function test_explicit_empty_user_scope_does_not_fall_back_to_all_pages(): void
|
||||
{
|
||||
$this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]);
|
||||
setting()->set('system.sidebar_permissions', [
|
||||
'configured' => ['user_'.$this->user->id], 'subjects' => ['user_'.$this->user->id => []],
|
||||
]);
|
||||
|
||||
@ -219,6 +219,7 @@ class AgreementSigningPageTest extends FeatureTestCase
|
||||
{
|
||||
$subject = 'user_' . $this->user->id;
|
||||
|
||||
$this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]);
|
||||
setting()->set('system.sidebar_permissions', json_encode([
|
||||
'configured' => [$subject],
|
||||
'subjects' => [
|
||||
|
||||
@ -254,6 +254,7 @@ class CourierQuoteChangeWorkflowTest extends FeatureTestCase
|
||||
public function test_user_without_quote_page_permission_cannot_use_workflow_routes(): void
|
||||
{
|
||||
$subject = 'user_' . $this->user->id;
|
||||
$this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]);
|
||||
setting()->set('system.sidebar_permissions', json_encode([
|
||||
'configured' => [$subject],
|
||||
'subjects' => [$subject => ['courier-payables/freight-reconciliation']],
|
||||
|
||||
@ -139,6 +139,7 @@ class ManagementCrudTest extends FeatureTestCase
|
||||
public function testInstallationChildEndpointRequiresTheSameSidebarPermission(): void
|
||||
{
|
||||
$this->withExceptionHandling();
|
||||
$this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]);
|
||||
setting()->set('system.sidebar_permissions', ['configured'=>['user_'.$this->user->id], 'subjects'=>['user_'.$this->user->id=>['store-management/basic-information']]]);
|
||||
$mirror=Mockery::mock(InstallationDetailMirrorRepository::class);
|
||||
$mirror->shouldNotReceive('details');
|
||||
|
||||
@ -308,6 +308,7 @@ class NonJingxiRefundPageTest extends FeatureTestCase
|
||||
public function test_page_rejects_users_without_sidebar_permission(): void
|
||||
{
|
||||
$subject = 'user_' . $this->user->id;
|
||||
$this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]);
|
||||
setting()->set('system.sidebar_permissions', json_encode([
|
||||
'configured' => [$subject],
|
||||
'subjects' => [
|
||||
|
||||
@ -331,6 +331,7 @@ class ProductCatalogPageTest extends FeatureTestCase
|
||||
{
|
||||
$subject = 'user_' . $this->user->id;
|
||||
|
||||
$this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]);
|
||||
setting()->set('system.sidebar_permissions', json_encode([
|
||||
'configured' => [$subject],
|
||||
'subjects' => [
|
||||
@ -348,6 +349,7 @@ class ProductCatalogPageTest extends FeatureTestCase
|
||||
{
|
||||
$subject = 'user_' . $this->user->id;
|
||||
|
||||
$this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]);
|
||||
setting()->set('system.sidebar_permissions', json_encode([
|
||||
'configured' => [$subject],
|
||||
'subjects' => [
|
||||
|
||||
@ -52,6 +52,7 @@ class PrototypePageTest extends FeatureTestCase
|
||||
|
||||
public function testNavigationUsesConfiguredPermissionsAndTitles(): void
|
||||
{
|
||||
$this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]);
|
||||
setting()->set('system.sidebar_permissions', [
|
||||
'configured' => ['user_' . $this->user->id],
|
||||
'subjects' => ['user_' . $this->user->id => ['company-management/subject-operating-report']],
|
||||
|
||||
@ -144,6 +144,7 @@ class QuoteManagementPageTest extends FeatureTestCase
|
||||
{
|
||||
$subject = 'user_' . $this->user->id;
|
||||
|
||||
$this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]);
|
||||
setting()->set('system.sidebar_permissions', json_encode([
|
||||
'configured' => [$subject],
|
||||
'subjects' => [
|
||||
|
||||
@ -155,6 +155,7 @@ class SmallPaymentWorkOrderPageTest extends FeatureTestCase
|
||||
public function test_page_rejects_users_without_sidebar_permission(): void
|
||||
{
|
||||
$subject = 'user_' . $this->user->id;
|
||||
$this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]);
|
||||
setting()->set('system.sidebar_permissions', json_encode([
|
||||
'configured' => [$subject],
|
||||
'subjects' => [$subject => ['company-management/basic-information']],
|
||||
|
||||
@ -77,6 +77,7 @@ class StoreAccountPasswordsTest extends FeatureTestCase
|
||||
|
||||
public function test_restricted_admin_cannot_bypass_store_page_scope(): void
|
||||
{
|
||||
$this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]);
|
||||
setting()->set('system.sidebar_permissions', ['configured' => ['user_'.$this->user->id], 'subjects' => ['user_'.$this->user->id => []]]);
|
||||
$this->mock(StoreAccountPasswordClient::class)->shouldNotReceive('reveal');
|
||||
$this->loginAs()->postJson($this->endpoint(), ['purpose' => 'view'])->assertForbidden();
|
||||
|
||||
@ -250,6 +250,7 @@ class SupplierPayablePagesTest extends FeatureTestCase
|
||||
{
|
||||
$subject = 'user_' . $this->user->id;
|
||||
|
||||
$this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]);
|
||||
setting()->set('system.sidebar_permissions', json_encode([
|
||||
'configured' => [$subject],
|
||||
'subjects' => [
|
||||
|
||||
144
tests/Feature/SystemSettings/UserPermissionsTest.php
Normal file
144
tests/Feature/SystemSettings/UserPermissionsTest.php
Normal file
@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\SystemSettings;
|
||||
|
||||
use App\Models\Auth\Permission;
|
||||
use App\Models\Auth\Role;
|
||||
use App\Models\Auth\User;
|
||||
use App\Utilities\BusinessNavigation;
|
||||
use App\Utilities\SidebarPermissions;
|
||||
use App\Utilities\SystemAccess;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
class UserPermissionsTest extends FeatureTestCase
|
||||
{
|
||||
public function test_admin_sees_all_company_accounts_but_cannot_edit_administrators(): void
|
||||
{
|
||||
$admin = $this->account('admin');
|
||||
$regular = $this->account('user');
|
||||
$outside = $this->account('user');
|
||||
$outside->companies()->detach();
|
||||
$this->loginAs($admin)->get(route('system-settings.users.index'))
|
||||
->assertOk()->assertSeeText($this->user->name)->assertSeeText($admin->name)->assertSeeText($regular->name)
|
||||
->assertDontSeeText($outside->name)
|
||||
->assertDontSee(route('system-settings.users.edit', $this->user->id), false)
|
||||
->assertSee(route('system-settings.users.edit', $regular->id), false);
|
||||
$this->withExceptionHandling()->get(route('system-settings.users.edit', $this->user->id))->assertForbidden();
|
||||
$this->patch(route('system-settings.users.update', $this->user->id), $this->payload($this->user, 'user'))->assertForbidden();
|
||||
$this->delete(route('system-settings.users.destroy', $this->user->id))->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_super_admin_can_assign_both_administrator_roles(): void
|
||||
{
|
||||
$target = $this->account('user');
|
||||
$this->loginAs()->get(route('system-settings.users.edit', $target->id))->assertOk()
|
||||
->assertSeeText('主管理员(最高权限)')->assertSee('value="admin"', false);
|
||||
foreach (['admin', 'super_admin'] as $role) {
|
||||
$this->patch(route('system-settings.users.update', $target->id), $this->payload($target, $role))
|
||||
->assertRedirect(route('system-settings.users.index'));
|
||||
$target = $target->fresh();
|
||||
$this->assertSame([$role], $target->roles()->pluck('name')->all());
|
||||
}
|
||||
setting()->set('system.sidebar_permissions', ['configured' => ['user_'.$target->id], 'subjects' => ['user_'.$target->id => []]]);
|
||||
$this->assertSame(BusinessNavigation::all(), SidebarPermissions::navigationFor($target));
|
||||
$this->assertTrue(SystemAccess::isSuperAdmin($target));
|
||||
$this->loginAs($target)->get(route('system-settings.users.index'))->assertOk();
|
||||
$this->get(route('settings.navigation.manage'))->assertOk();
|
||||
}
|
||||
|
||||
public function test_admin_cannot_promote_a_user_or_itself(): void
|
||||
{
|
||||
$admin = $this->account('admin');
|
||||
$target = $this->account('user');
|
||||
$this->withExceptionHandling()->loginAs($admin);
|
||||
foreach (['admin', 'super_admin'] as $role) {
|
||||
$this->patchJson(route('system-settings.users.update', $target->id), $this->payload($target, $role))
|
||||
->assertUnprocessable()->assertJsonValidationErrors('role');
|
||||
}
|
||||
$this->patchJson(route('system-settings.users.update', $admin->id), $this->payload($admin, 'super_admin'))->assertForbidden();
|
||||
$this->assertSame(['user'], $target->roles()->pluck('name')->all());
|
||||
}
|
||||
|
||||
public function test_demotion_removes_cached_administrator_access(): void
|
||||
{
|
||||
$target = $this->account('super_admin');
|
||||
$this->assertTrue($target->hasRole('super_admin'));
|
||||
$this->assertTrue($target->can('update-auth-users'));
|
||||
$this->loginAs()->patch(route('system-settings.users.update', $target->id), $this->payload($target, 'user'))
|
||||
->assertRedirect(route('system-settings.users.index'));
|
||||
$this->assertSame('user', SystemAccess::roleName($target));
|
||||
$this->assertFalse($target->fresh()->can('update-auth-users'));
|
||||
$this->withExceptionHandling()->loginAs($target)->get(route('system-settings.users.index'))->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_legacy_user_routes_cannot_grant_administrator_identity(): void
|
||||
{
|
||||
$admin = $this->account('admin');
|
||||
$target = $this->account('user');
|
||||
$this->withExceptionHandling()->loginAs($admin);
|
||||
$role = (string) Role::where('name', 'super_admin')->value('id');
|
||||
$this->patchJson(route('users.update', $target->id), ['roles' => $role])->assertForbidden();
|
||||
$this->patchJson(route('profile.update', $admin->id), ['roles' => $role])->assertForbidden();
|
||||
$this->postJson(route('users.store'), ['roles' => $role])->assertForbidden();
|
||||
$this->patchJson(route('users.update', $this->user->id), ['name' => 'unauthorized'])->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_last_super_admin_cannot_be_demoted_or_disabled(): void
|
||||
{
|
||||
$this->loginAs();
|
||||
$this->patch(route('system-settings.users.update', $this->user->id), $this->payload($this->user, 'admin'))
|
||||
->assertSessionHasErrors('role');
|
||||
$this->patch(route('system-settings.users.update', $this->user->id), array_merge($this->payload($this->user, 'super_admin'), ['enabled' => 0]))
|
||||
->assertSessionHasErrors('enabled');
|
||||
$this->assertSame(['super_admin'], $this->user->roles()->pluck('name')->all());
|
||||
}
|
||||
|
||||
public function test_user_page_settings_are_a_second_level_page_and_save_for_selected_user(): void
|
||||
{
|
||||
$target = $this->account('user');
|
||||
$this->loginAs()->get(route('system-settings.permissions.index'))->assertOk()
|
||||
->assertSeeText('用户可访问页面')->assertSee(route('system-settings.sidebar.edit'), false);
|
||||
$this->get(route('system-settings.sidebar.edit', ['subject' => 'user_'.$target->id]))->assertOk()
|
||||
->assertSee('aria-label="页面位置"', false)->assertSeeText('主体商标规则');
|
||||
$this->patch(route('system-settings.sidebar.update'), [
|
||||
'subject' => 'user_'.$target->id, 'permissions' => ['store-management/basic-information'],
|
||||
])->assertRedirect(route('system-settings.sidebar.edit', ['subject' => 'user_'.$target->id]));
|
||||
$this->assertSame(['store-management/basic-information'], SidebarPermissions::allowedKeys($target));
|
||||
$this->get(route('system-settings.sidebar.edit', ['subject' => 'user_'.$target->id]))->assertOk()->assertSeeText('自定义权限');
|
||||
}
|
||||
|
||||
public function test_super_admin_cannot_be_limited_by_page_settings_and_regular_users_cannot_configure_access(): void
|
||||
{
|
||||
$this->withExceptionHandling()->loginAs()->patchJson(route('system-settings.sidebar.update'), [
|
||||
'subject' => 'user_'.$this->user->id, 'permissions' => [],
|
||||
])->assertUnprocessable();
|
||||
$regular = $this->account('user');
|
||||
$this->loginAs($regular)->get(route('system-settings.permissions.index'))->assertForbidden();
|
||||
$this->get(route('system-settings.users.index'))->assertForbidden();
|
||||
$this->patchJson(route('system-settings.sidebar.update'), ['subject' => 'role_user', 'permissions' => []])->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_child_page_navigation_contains_only_the_selected_child(): void
|
||||
{
|
||||
$regular = $this->account('user');
|
||||
setting()->set('system.sidebar_permissions', ['configured' => ['user_'.$regular->id], 'subjects' => ['user_'.$regular->id => ['trademark-management/subject-trademark-rules']]]);
|
||||
$navigation = SidebarPermissions::navigationFor($regular);
|
||||
$pages = collect($navigation)->flatMap(fn ($section) => $section['pages'])->flatMap(fn ($page) => $page['children'] ?? [$page]);
|
||||
$this->assertSame(['subject-trademark-rules'], $pages->pluck('slug')->all());
|
||||
}
|
||||
|
||||
private function account(string $role): User
|
||||
{
|
||||
$name = 'fixture-'.$role.'-'.User::count();
|
||||
$account = User::create(['name' => $name, 'email' => $name.'@example.test', 'password' => 'fixture-unused-password', 'enabled' => true, 'locale' => 'zh-CN', 'landing_page' => 'dashboard']);
|
||||
$account->roles()->attach(Role::where('name', $role)->firstOrFail()->id);
|
||||
$account->permissions()->attach(Permission::where('name', 'read-admin-panel')->firstOrFail()->id);
|
||||
$account->companies()->attach($this->company->id);
|
||||
return $account;
|
||||
}
|
||||
|
||||
private function payload(User $target, string $role): array
|
||||
{
|
||||
return ['name' => $target->name, 'email' => $target->email, 'role' => $role, 'enabled' => 1];
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user