fix: 补全用户分组入口并统一侧边栏字号
This commit is contained in:
parent
d876822a6e
commit
10d614f712
@ -35,6 +35,46 @@ class UserGroups extends Controller
|
||||
return $this->form(UserAccessGroup::forCompany()->findOrFail($group));
|
||||
}
|
||||
|
||||
public function editMembership(int $user)
|
||||
{
|
||||
$member = $this->memberForCompany($user);
|
||||
$groups = UserAccessGroup::forCompany()->orderBy('name')->get();
|
||||
|
||||
return view('system_settings.groups.membership', [
|
||||
'member' => $member,
|
||||
'groups' => $groups,
|
||||
'selected' => UserAccessGroup::forUser($member)->pluck('id')->all(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateMembership(int $user, Request $request)
|
||||
{
|
||||
$member = $this->memberForCompany($user);
|
||||
$availableGroupIds = UserAccessGroup::forCompany()->pluck('id')->all();
|
||||
$data = $request->validate([
|
||||
'groups' => ['nullable', 'array'],
|
||||
'groups.*' => ['integer', 'distinct', Rule::in($availableGroupIds)],
|
||||
], [], ['groups' => '用户分组', 'groups.*' => '用户分组']);
|
||||
$selected = array_values($data['groups'] ?? []);
|
||||
|
||||
DB::transaction(function () use ($member, $availableGroupIds, $selected) {
|
||||
DB::table('user_access_group_members')
|
||||
->where('user_id', $member->id)
|
||||
->whereIn('group_id', $availableGroupIds)
|
||||
->delete();
|
||||
if ($selected !== []) {
|
||||
DB::table('user_access_group_members')->insert(array_map(
|
||||
fn ($groupId) => ['group_id' => $groupId, 'user_id' => $member->id],
|
||||
$selected
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
flash('用户分组已更新。')->success();
|
||||
|
||||
return redirect()->route('system-settings.users.index');
|
||||
}
|
||||
|
||||
private function form(?UserAccessGroup $group = null)
|
||||
{
|
||||
return view('system_settings.groups.form', ['group' => $group, 'members' => $this->members(), 'selected' => $group?->users()->pluck('users.id')->all() ?? []]);
|
||||
@ -86,4 +126,12 @@ class UserGroups extends Controller
|
||||
->whereHas('roles', fn ($query) => $query->whereIn('name', ['admin', 'user']))
|
||||
->whereDoesntHave('roles', fn ($query) => $query->where('name', 'super_admin'))->orderBy('name')->get();
|
||||
}
|
||||
|
||||
private function memberForCompany(int $user): User
|
||||
{
|
||||
$member = $this->members()->firstWhere('id', $user);
|
||||
abort_unless($member, 404);
|
||||
|
||||
return $member;
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ class Users extends Controller
|
||||
|
||||
public function index()
|
||||
{
|
||||
$users = $this->queryForActor()->with('roles')->orderBy('id')->get();
|
||||
$users = $this->queryForActor()->with(['roles', 'dingtalkIdentity'])->orderBy('id')->get();
|
||||
$groupNamesByUser = [];
|
||||
foreach (UserAccessGroup::forCompany()->with('users')->get() as $group) {
|
||||
foreach ($group->users as $member) {
|
||||
@ -142,6 +142,10 @@ class Users extends Controller
|
||||
return back()->withErrors(['user' => '系统至少需要保留一名启用的主管理员。']);
|
||||
}
|
||||
|
||||
if ($managedUser->dingtalkIdentity()->exists()) {
|
||||
return back()->withErrors(['user' => '钉钉绑定用户不能直接删除。需要停止访问时请停用账号,避免钉钉身份残留后无法再次登录。']);
|
||||
}
|
||||
|
||||
$managedUser->delete();
|
||||
|
||||
flash('用户已删除。')->success();
|
||||
|
||||
5
public/css/app.css
vendored
5
public/css/app.css
vendored
File diff suppressed because one or more lines are too long
36
resources/assets/sass/app.css
vendored
36
resources/assets/sass/app.css
vendored
@ -363,6 +363,42 @@ body {
|
||||
line-height: 1rem !important;
|
||||
}
|
||||
|
||||
.finance-theme-sidebar .main-menu #sidenav-collapse-main > ul > li,
|
||||
.finance-theme-sidebar .main-menu #sidenav-collapse-main > ul > details {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.finance-theme-sidebar .main-menu #sidenav-collapse-main > ul > li > a,
|
||||
.finance-theme-sidebar .main-menu #sidenav-collapse-main > ul > details > summary > div {
|
||||
min-height: 2.4rem;
|
||||
margin: 0.125rem 0;
|
||||
padding: 0.55rem 0.75rem !important;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem !important;
|
||||
line-height: 1rem !important;
|
||||
}
|
||||
|
||||
.finance-theme-sidebar .main-menu #sidenav-collapse-main > ul > li > a > div,
|
||||
.finance-theme-sidebar .main-menu #sidenav-collapse-main > ul > details > summary > div > div {
|
||||
width: 1.25rem !important;
|
||||
height: 1.25rem !important;
|
||||
margin-inline-end: 0.65rem !important;
|
||||
}
|
||||
|
||||
.finance-theme-sidebar .main-menu #sidenav-collapse-main > ul > li > a .material-icons,
|
||||
.finance-theme-sidebar .main-menu #sidenav-collapse-main > ul > li > a .material-icons-outlined,
|
||||
.finance-theme-sidebar .main-menu #sidenav-collapse-main > ul > details > summary > div .material-icons,
|
||||
.finance-theme-sidebar .main-menu #sidenav-collapse-main > ul > details > summary > div .material-icons-outlined {
|
||||
font-size: 1rem !important;
|
||||
line-height: 1rem !important;
|
||||
}
|
||||
|
||||
.finance-theme-sidebar .main-menu #sidenav-collapse-main > ul > details > summary > div > .material-icons:last-child {
|
||||
right: auto !important;
|
||||
left: auto !important;
|
||||
inset-inline-end: 0.75rem !important;
|
||||
}
|
||||
|
||||
.finance-theme-sidebar .main-menu a.font-semibold,
|
||||
.finance-theme-sidebar .main-menu summary.font-semibold,
|
||||
.finance-theme-sidebar .main-menu div.font-semibold {
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<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.users.index') }}">用户管理</a><span class="mx-2">/</span><span aria-current="page">用户分组</span></nav>
|
||||
<p class="mb-4 text-sm text-gray-600">用户可加入多个分组,获得各组页面权限的并集。分组不授予管理员身份;用户单独配置优先。</p>
|
||||
<p class="mb-4 text-sm text-gray-600">用户可加入多个分组,获得各组页面权限的并集。可在这里按分组批量维护成员,也可从“用户管理”逐人加入现有分组。</p>
|
||||
@if ($errors->any())<div role="alert" class="mb-4 text-sm text-red-700">{{ $errors->first() }}</div>@endif
|
||||
<div class="overflow-x-auto rounded-xl border border-gray-200 bg-white">
|
||||
<table class="min-w-full text-sm text-left">
|
||||
@ -17,7 +17,7 @@
|
||||
<td class="px-5 py-4">{{ $group->users_count }}</td>
|
||||
<td class="px-5 py-4">{{ empty($group->page_permissions) ? '未开放页面' : '已配置' }}</td>
|
||||
<td class="px-5 py-4 text-right whitespace-nowrap">
|
||||
<a class="settings-accent mr-3" href="{{ route('system-settings.groups.edit', $group->id) }}">管理成员</a>
|
||||
<a class="settings-accent mr-3" href="{{ route('system-settings.groups.edit', $group->id) }}">添加/移除成员</a>
|
||||
<a class="settings-accent mr-3" href="{{ route('system-settings.sidebar.edit', ['subject' => 'group_'.$group->id]) }}">设置页面权限</a>
|
||||
@if ($group->users_count === 0)
|
||||
<form class="inline" method="POST" action="{{ route('system-settings.groups.destroy', $group->id) }}" onsubmit="return confirm('确定删除此空分组吗?')">
|
||||
|
||||
32
resources/views/system_settings/groups/membership.blade.php
Normal file
32
resources/views/system_settings/groups/membership.blade.php
Normal file
@ -0,0 +1,32 @@
|
||||
<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')
|
||||
<nav class="mb-4 text-sm" aria-label="页面位置"><a class="settings-accent" href="{{ route('system-settings.users.index') }}">用户管理</a><span class="mx-2">/</span><span aria-current="page">管理分组</span></nav>
|
||||
<form method="POST" action="{{ route('system-settings.groups.membership.update', $member->id) }}" class="rounded-xl border border-gray-200 bg-white p-6">
|
||||
@csrf @method('PATCH')
|
||||
@if ($errors->any())<div role="alert" class="mb-4 text-sm text-red-700">@foreach($errors->all() as $error)<p>{{ $error }}</p>@endforeach</div>@endif
|
||||
<div class="border-b border-gray-200 pb-5">
|
||||
<h2 class="text-base font-semibold text-gray-900">{{ $member->name }}</h2>
|
||||
<p class="mt-1 break-all text-sm text-gray-600">{{ $member->email }}</p>
|
||||
<p class="mt-3 text-sm text-gray-600">勾选一个或多个现有分组。用户会获得各分组页面权限的并集;用户单独配置的页面权限仍然优先。</p>
|
||||
</div>
|
||||
<fieldset class="mt-6">
|
||||
<legend class="text-sm font-semibold">现有分组</legend>
|
||||
<div class="mt-4 grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
@forelse ($groups as $group)
|
||||
<label class="flex items-start gap-3 rounded-lg border border-gray-200 px-4 py-3 text-sm hover:bg-gray-50">
|
||||
<input type="checkbox" name="groups[]" value="{{ $group->id }}" @checked(in_array($group->id, old('groups', $errors->any() ? [] : $selected))) class="mt-1 rounded settings-accent settings-field-focus">
|
||||
<span><span class="font-semibold text-gray-800">{{ $group->name }}</span><span class="mt-1 block text-xs text-gray-600">{{ empty($group->page_permissions) ? '暂未开放页面' : count($group->page_permissions).' 项页面权限' }}</span></span>
|
||||
</label>
|
||||
@empty
|
||||
<div class="md:col-span-2 xl:col-span-3 rounded-lg bg-gray-50 px-4 py-6 text-sm text-gray-600">还没有可加入的分组。请先<a class="settings-accent hover:underline" href="{{ route('system-settings.groups.create') }}">创建分组</a>。</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="mt-6 flex justify-end gap-4 border-t border-gray-200 pt-4"><a class="settings-accent" href="{{ route('system-settings.users.index') }}">取消</a><button class="settings-primary" type="submit">保存用户分组</button></div>
|
||||
</form>
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-layouts.admin>
|
||||
@ -6,7 +6,7 @@
|
||||
window.caiwuSettingsNavigation = true;
|
||||
let pending;
|
||||
const supports = url => url.origin === location.origin
|
||||
&& /\/system-settings\/(?:(?:users|groups)(?:\/create|\/\d+\/edit)?|permissions|sidebar)$/.test(url.pathname);
|
||||
&& /\/system-settings\/(?:(?:users|groups)(?:\/create|\/\d+\/edit)?|users\/\d+\/groups|permissions|sidebar)$/.test(url.pathname);
|
||||
async function navigate(url, push = true) {
|
||||
const current = document.querySelector('[data-settings-workspace]');
|
||||
if (!current) return;
|
||||
|
||||
@ -68,9 +68,12 @@
|
||||
@if (\App\Utilities\SystemAccess::canManageUser(user(), $managedUser))
|
||||
<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))
|
||||
@if (\App\Utilities\SystemAccess::isSuperAdmin())
|
||||
<a href="{{ route('system-settings.groups.membership.edit', $managedUser->id) }}" class="mr-4 settings-accent hover:underline">管理分组</a>
|
||||
@endif
|
||||
<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)
|
||||
@if ($managedUser->id !== user()->id && ! $managedUser->dingtalkIdentity)
|
||||
<form method="POST" action="{{ route('system-settings.users.destroy', $managedUser->id) }}" class="inline" onsubmit="return confirm('确定删除该用户吗?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
|
||||
@ -176,6 +176,8 @@ Route::group(['as' => 'system-settings.', 'prefix' => 'system-settings'], functi
|
||||
Route::get('users/create', 'SystemSettings\Users@create')->name('users.create');
|
||||
Route::post('users', 'SystemSettings\Users@store')->name('users.store');
|
||||
Route::get('users/{user}/edit', 'SystemSettings\Users@edit')->name('users.edit');
|
||||
Route::get('users/{user}/groups', 'SystemSettings\UserGroups@editMembership')->name('groups.membership.edit');
|
||||
Route::patch('users/{user}/groups', 'SystemSettings\UserGroups@updateMembership')->name('groups.membership.update');
|
||||
Route::patch('users/{user}', 'SystemSettings\Users@update')->name('users.update');
|
||||
Route::delete('users/{user}', 'SystemSettings\Users@destroy')->name('users.destroy');
|
||||
|
||||
|
||||
@ -24,6 +24,40 @@ class UserAccessGroupsTest extends FeatureTestCase
|
||||
$this->get(route('system-settings.groups.index'))->assertOk()->assertSeeText('未开放页面');
|
||||
}
|
||||
|
||||
public function test_later_user_can_be_added_to_existing_groups_from_user_management(): void
|
||||
{
|
||||
$first = $this->member();
|
||||
$group = $this->group('现有核算组', $first, ['store-management/basic-information']);
|
||||
$group->users()->detach($first->id);
|
||||
$later = $this->member();
|
||||
|
||||
$this->loginAs()->get(route('system-settings.users.index'))->assertOk()
|
||||
->assertSee(route('system-settings.groups.membership.edit', $later->id), false)
|
||||
->assertSeeText('管理分组');
|
||||
$this->get(route('system-settings.groups.membership.edit', $later->id))->assertOk()
|
||||
->assertSeeText('现有核算组')->assertSeeText($later->name);
|
||||
$this->patch(route('system-settings.groups.membership.update', $later->id), ['groups' => [$group->id]])
|
||||
->assertRedirect(route('system-settings.users.index'));
|
||||
|
||||
$this->assertTrue($group->users()->whereKey($later->id)->exists());
|
||||
$this->assertSame(['store-management/basic-information'], SidebarPermissions::allowedKeys($later));
|
||||
}
|
||||
|
||||
public function test_user_membership_editor_is_company_scoped_and_super_admin_only(): void
|
||||
{
|
||||
$member = $this->member();
|
||||
$group = $this->group('本公司分组', $member);
|
||||
$admin = $this->member('admin');
|
||||
|
||||
$this->withExceptionHandling()->loginAs($admin)
|
||||
->get(route('system-settings.groups.membership.edit', $member->id))->assertForbidden();
|
||||
$this->loginAs()->withHeaders(['X-Requested-With' => 'XMLHttpRequest'])
|
||||
->get(route('system-settings.groups.membership.edit', $this->user->id))->assertNotFound();
|
||||
$this->patchJson(route('system-settings.groups.membership.update', $member->id), ['groups' => [$group->id + 9999]])
|
||||
->assertUnprocessable();
|
||||
$this->assertSame([$group->id], UserAccessGroup::forUser($member)->pluck('id')->all());
|
||||
}
|
||||
|
||||
public function test_multiple_group_permissions_are_unioned_and_enforced_on_real_routes(): void
|
||||
{
|
||||
$member = $this->member();
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace Tests\Feature\SystemSettings;
|
||||
|
||||
use App\Models\Auth\DingTalkIdentity;
|
||||
use App\Models\Auth\Permission;
|
||||
use App\Models\Auth\Role;
|
||||
use App\Models\Auth\User;
|
||||
@ -127,6 +128,22 @@ class UserPermissionsTest extends FeatureTestCase
|
||||
$this->assertSame(['subject-trademark-rules'], $pages->pluck('slug')->all());
|
||||
}
|
||||
|
||||
public function test_dingtalk_bound_user_cannot_be_deleted_into_an_orphaned_identity(): void
|
||||
{
|
||||
$target = $this->account('user');
|
||||
DingTalkIdentity::query()->create([
|
||||
'user_id' => $target->id,
|
||||
'union_id' => 'fixture-delete-guard-union',
|
||||
'dingtalk_user_id' => 'fixture-delete-guard-user',
|
||||
'nickname' => '在职财务',
|
||||
]);
|
||||
|
||||
$this->loginAs()->delete(route('system-settings.users.destroy', $target->id))
|
||||
->assertSessionHasErrors('user');
|
||||
$this->assertNotNull($target->fresh());
|
||||
$this->assertDatabaseHas('dingtalk_identities', ['user_id' => $target->id]);
|
||||
}
|
||||
|
||||
private function account(string $role): User
|
||||
{
|
||||
$name = 'fixture-'.$role.'-'.User::count();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user