1. 主帳號不變量:storeAccount 的 is_admin 改補位(每家公司恰一主帳號)、destroyAccount 禁刪最後主帳號、roles 新增 is_company_admin 旗標保護公司管理員角色不被刪除 2. 帳號樹狀層級:users 新增 parent_id/level(主帳號 level 0、子帳號最多兩層),新增 visibleTo/canManageAccount,帳號清單與管理端點(accounts/update/destroy/toggle、機台授權選帳號、補貨指派)一律套層級可見範圍與越權防護 3. 角色指派子集:storeAccount/updateAccount 指派角色與帳號 Modal 下拉一律限制為「權限為操作者子集」,杜絕權限提升 4. 機台授權子集:MachinePermissionController 僅允許在操作者本身可授權機台範圍內增刪,保留範圍外既有授權,並修正 machine_access 全域 scope 對讀寫的干擾 5. 角色層級隔離:roles 新增 created_by,子帳號只看/只管/只能指派自己建立的角色(主帳號看全公司),edit/update/destroy 加 canBeManagedBy 守衛 6. 補強:MachineSetting 機台權限人員清單補租戶隔離、修正角色下拉 N+1、三語系新增提示字串 7. 測試:新增 AccountHierarchyTest/AccountSubsetGuardTest/RoleHierarchyTest,Admin 套件 55 測試 0 回歸 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
703 lines
31 KiB
PHP
703 lines
31 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers\Admin;
|
||
|
||
use App\Http\Controllers\Controller;
|
||
use Illuminate\Http\Request;
|
||
|
||
class PermissionController extends Controller
|
||
{
|
||
// 權限角色設定
|
||
public function roles()
|
||
{
|
||
$per_page = request()->input('roles_per_page', request()->input('per_page', 10));
|
||
$user = auth()->user();
|
||
$query = \App\Models\System\Role::query()->with(['permissions', 'users', 'company']);
|
||
|
||
// 層級隔離:系統管理員看全部;主帳號看同公司全部;子帳號僅看自己建立的角色。
|
||
$query->visibleTo($user);
|
||
|
||
// 搜尋:角色名稱(支援 roles_search 命名空間 與舊版 search)
|
||
$search = request()->input('roles_search', request()->input('search'));
|
||
if ($search) {
|
||
$query->where('name', 'like', "%{$search}%");
|
||
}
|
||
|
||
// 篩選:公司名稱(支援 roles_company_id 命名空間 與舊版 company_id)
|
||
$company_id = request()->input('roles_company_id', request()->input('company_id'));
|
||
if ($user->isSystemAdmin() && filled($company_id)) {
|
||
if ($company_id === 'system') {
|
||
$query->whereNull('company_id');
|
||
} else {
|
||
$query->where('company_id', $company_id);
|
||
}
|
||
}
|
||
|
||
$roles = $query->latest()->paginate($per_page, ['*'], 'roles_page')->withQueryString();
|
||
$companies = $user->isSystemAdmin() ? \App\Models\System\Company::all() : collect();
|
||
|
||
// 權限分組邏輯中的標題與過濾
|
||
$isSubAccountRoles = request()->routeIs('*.sub-account-roles');
|
||
$title = $isSubAccountRoles ? __('Sub Account Roles') : __('Role Settings');
|
||
$permissionQuery = \Spatie\Permission\Models\Permission::query();
|
||
if (!$user->isSystemAdmin()) {
|
||
$permissionQuery->whereIn('name', $user->getAllPermissions()->pluck('name'));
|
||
}
|
||
|
||
// 權限分組邏輯
|
||
$all_permissions = $permissionQuery->get()
|
||
->reject(fn($p) => $p->name === 'menu.data-config.sub-account-roles')
|
||
->groupBy(function($perm) {
|
||
if (str_starts_with($perm->name, 'menu.')) {
|
||
return 'menu';
|
||
}
|
||
return 'other';
|
||
});
|
||
|
||
$currentUserRoleIds = $user->roles->pluck('id')->toArray();
|
||
return view('admin.permission.roles', compact('roles', 'all_permissions', 'title', 'currentUserRoleIds', 'companies'));
|
||
}
|
||
|
||
/**
|
||
* Show the form for creating a new role.
|
||
*/
|
||
public function createRole()
|
||
{
|
||
$role = new \App\Models\System\Role();
|
||
$user = auth()->user();
|
||
|
||
// 權限遞迴約束由 getFilteredPermissions 處理
|
||
|
||
$all_permissions = $this->getFilteredPermissions($user);
|
||
|
||
$title = request()->routeIs('*.sub-account-roles.create') ? __('Create Sub Account Role') : __('Create New Role');
|
||
$back_url = request()->routeIs('*.sub-account-roles.create')
|
||
? route('admin.data-config.sub-accounts', ['tab' => 'roles'])
|
||
: route('admin.permission.roles');
|
||
|
||
return view('admin.permission.roles-edit', compact('role', 'all_permissions', 'title', 'back_url'));
|
||
}
|
||
|
||
/**
|
||
* Show the form for editing the specified role.
|
||
*/
|
||
public function editRole($id)
|
||
{
|
||
$role = \App\Models\System\Role::findOrFail($id);
|
||
$user = auth()->user();
|
||
|
||
// 層級隔離:子帳號只能編輯自己建立的角色,不可開啟上層/旁線建立的角色。
|
||
if (!$role->canBeManagedBy($user)) {
|
||
return redirect()->back()->with('error', __('You do not have permission to manage this role.'));
|
||
}
|
||
|
||
// 權限遞迴約束與分組邏輯由 getFilteredPermissions 處理
|
||
|
||
$all_permissions = $this->getFilteredPermissions($user);
|
||
|
||
// 根據路由決定標題
|
||
$title = request()->routeIs('*.sub-account-roles.edit') ? __('Edit Sub Account Role') : __('Edit Role Permissions');
|
||
|
||
// 麵包屑/返回路徑
|
||
$back_url = request()->routeIs('*.sub-account-roles.edit')
|
||
? route('admin.data-config.sub-accounts', ['tab' => 'roles'])
|
||
: route('admin.permission.roles');
|
||
|
||
return view('admin.permission.roles-edit', compact('role', 'all_permissions', 'title', 'back_url'));
|
||
}
|
||
|
||
/**
|
||
* Store a newly created role in storage.
|
||
*/
|
||
public function storeRole(Request $request)
|
||
{
|
||
$is_system = auth()->user()->isSystemAdmin() && $request->boolean('is_system');
|
||
$company_id = $is_system ? null : auth()->user()->company_id;
|
||
|
||
$validated = $request->validate([
|
||
'name' => [
|
||
'required', 'string', 'max:255',
|
||
\Illuminate\Validation\Rule::unique('roles', 'name')->where(function ($query) use ($company_id) {
|
||
return $query->where('company_id', $company_id);
|
||
})
|
||
],
|
||
'permissions' => 'nullable|array',
|
||
'permissions.*' => 'string|exists:permissions,name',
|
||
]);
|
||
|
||
$role = \App\Models\System\Role::query()->create([
|
||
'name' => $validated['name'],
|
||
'guard_name' => 'web',
|
||
'company_id' => $is_system ? null : auth()->user()->company_id,
|
||
'created_by' => auth()->id(),
|
||
'is_system' => $is_system,
|
||
]);
|
||
|
||
if (!empty($validated['permissions'])) {
|
||
$perms = $validated['permissions'];
|
||
|
||
// 權限遞迴約束驗證
|
||
if (!auth()->user()->isSystemAdmin()) {
|
||
$currentUserPerms = auth()->user()->getAllPermissions()->pluck('name');
|
||
if (collect($perms)->diff($currentUserPerms)->isNotEmpty()) {
|
||
return redirect()->back()->with('error', __('You cannot assign permissions you do not possess.'));
|
||
}
|
||
}
|
||
|
||
// 如果不是系統角色,排除主選單的系統權限
|
||
if (!$is_system) {
|
||
$perms = array_diff($perms, ['menu.basic', 'menu.permissions']);
|
||
}
|
||
$role->syncPermissions($perms);
|
||
}
|
||
|
||
$target_route = request()->routeIs('*.sub-account-roles.*') ? route('admin.data-config.sub-accounts', ['tab' => 'roles']) : route('admin.permission.roles');
|
||
return redirect()->to($target_route)->with('success', __('Role created successfully.'));
|
||
}
|
||
|
||
/**
|
||
* Update the specified role in storage.
|
||
*/
|
||
public function updateRole(Request $request, $id)
|
||
{
|
||
$role = \App\Models\System\Role::findOrFail($id);
|
||
|
||
$is_system = $role->is_system;
|
||
$company_id = $role->company_id;
|
||
|
||
$validated = $request->validate([
|
||
'name' => [
|
||
'required', 'string', 'max:255',
|
||
\Illuminate\Validation\Rule::unique('roles', 'name')
|
||
->ignore($id)
|
||
->where(function ($query) use ($company_id) {
|
||
return $query->where('company_id', $company_id);
|
||
})
|
||
],
|
||
'permissions' => 'nullable|array',
|
||
'permissions.*' => 'string|exists:permissions,name',
|
||
]);
|
||
|
||
if ($role->name === 'super-admin') {
|
||
return redirect()->back()->with('error', __('The Super Admin role is immutable.'));
|
||
}
|
||
|
||
if (!auth()->user()->isSystemAdmin() && $role->is_system) {
|
||
return redirect()->back()->with('error', __('System roles cannot be modified by tenant administrators.'));
|
||
}
|
||
|
||
// 層級隔離:子帳號只能修改自己建立的角色。
|
||
if (!$role->canBeManagedBy(auth()->user())) {
|
||
return redirect()->back()->with('error', __('You do not have permission to manage this role.'));
|
||
}
|
||
|
||
$is_system = auth()->user()->isSystemAdmin() ? $request->boolean('is_system') : $role->is_system;
|
||
|
||
$updateData = [
|
||
'name' => $validated['name'],
|
||
'is_system' => $is_system,
|
||
'company_id' => $is_system ? null : $role->company_id,
|
||
];
|
||
|
||
$role->update($updateData);
|
||
|
||
$perms = $validated['permissions'] ?? [];
|
||
|
||
// 權限遞迴約束驗證
|
||
if (!auth()->user()->isSystemAdmin()) {
|
||
$currentUserPerms = auth()->user()->getAllPermissions()->pluck('name');
|
||
if (collect($perms)->diff($currentUserPerms)->isNotEmpty()) {
|
||
return redirect()->back()->with('error', __('You cannot assign permissions you do not possess.'));
|
||
}
|
||
}
|
||
|
||
// 如果不是系統角色,排除主選單的系統權限
|
||
if (!$is_system) {
|
||
$perms = array_diff($perms, ['menu.basic', 'menu.permissions']);
|
||
}
|
||
$role->syncPermissions($perms);
|
||
|
||
$target_route = request()->routeIs('*.sub-account-roles.*') ? route('admin.data-config.sub-accounts', ['tab' => 'roles']) : route('admin.permission.roles');
|
||
return redirect()->to($target_route)->with('success', __('Role updated successfully.'));
|
||
}
|
||
|
||
/**
|
||
* Remove the specified role from storage.
|
||
*/
|
||
public function destroyRole($id)
|
||
{
|
||
$role = \App\Models\System\Role::findOrFail($id);
|
||
|
||
if ($role->name === 'super-admin') {
|
||
return redirect()->back()->with('error', __('The Super Admin role cannot be deleted.'));
|
||
}
|
||
|
||
// 公司主帳號角色保護:比照 super-admin,公司層級的「管理員」角色為該公司最高權限角色,不可刪除。
|
||
if ($role->is_company_admin) {
|
||
return redirect()->back()->with('error', __('The company admin role cannot be deleted.'));
|
||
}
|
||
|
||
if (!auth()->user()->isSystemAdmin() && $role->is_system) {
|
||
return redirect()->back()->with('error', __('System roles cannot be deleted by tenant administrators.'));
|
||
}
|
||
|
||
// 層級隔離:子帳號只能刪除自己建立的角色。
|
||
if (!$role->canBeManagedBy(auth()->user())) {
|
||
return redirect()->back()->with('error', __('You do not have permission to manage this role.'));
|
||
}
|
||
|
||
if ($role->users()->count() > 0) {
|
||
return redirect()->back()->with('error', __('Cannot delete role with active users.'));
|
||
}
|
||
|
||
$role->delete();
|
||
|
||
if (request()->routeIs('*.sub-account-roles.*')) {
|
||
return redirect()->route('admin.data-config.sub-accounts', ['tab' => 'roles'])->with('success', __('Role deleted successfully.'));
|
||
}
|
||
|
||
return redirect()->back()->with('success', __('Role deleted successfully.'));
|
||
}
|
||
|
||
// 帳號管理
|
||
public function accounts(Request $request)
|
||
{
|
||
$user = auth()->user();
|
||
$isSubAccountRoute = $request->routeIs('admin.data-config.sub-accounts');
|
||
$tab = $request->input('tab', 'accounts'); // 僅用於設定 Alpine activeTab 初始值
|
||
|
||
$companies = $user->isSystemAdmin() ? \App\Models\System\Company::all() : collect();
|
||
$currentUserRoleIds = $user->roles->pluck('id')->toArray();
|
||
|
||
// ── 帳號列表 ──────────────────────────────────────────────────
|
||
// 可見範圍:系統管理員看全部;主帳號看同公司全部;子帳號僅看自己直接建立的下層。
|
||
$usersQuery = \App\Models\System\User::query()->with(['company', 'roles', 'machines'])->visibleTo($user);
|
||
if ($search = $request->input('accounts_search', $request->input('search'))) {
|
||
$usersQuery->where(function ($q) use ($search) {
|
||
$q->where('name', 'like', "%{$search}%")
|
||
->orWhere('username', 'like', "%{$search}%")
|
||
->orWhere('email', 'like', "%{$search}%");
|
||
});
|
||
}
|
||
if ($user->isSystemAdmin() && $request->filled('accounts_company_id')) {
|
||
$cid = $request->input('accounts_company_id');
|
||
$cid === 'system' ? $usersQuery->whereNull('company_id') : $usersQuery->where('company_id', $cid);
|
||
}
|
||
$accounts_per_page = $request->input('accounts_per_page', 10);
|
||
$users = $usersQuery->latest()->paginate($accounts_per_page, ['*'], 'accounts_page')->withQueryString();
|
||
|
||
// Modal 用的角色選單(不分頁)。層級隔離:子帳號僅見自己建立的角色(與角色清單一致)。
|
||
// 預先載入 permissions 以避免下方子集過濾造成 N+1。
|
||
$rolesForSelect = \App\Models\System\Role::query()->with('permissions')->visibleTo($user);
|
||
$roles = $rolesForSelect->get();
|
||
// 子集過濾:租戶只能指派「權限為自身子集」的角色(與 store/update 的後端 guard 一致,避免下拉出現越權選項)。
|
||
if (!$user->isSystemAdmin()) {
|
||
$operatorPerms = $user->getAllPermissions()->pluck('name');
|
||
$roles = $roles->filter(
|
||
fn($r) => collect($r->getPermissionNames())->diff($operatorPerms)->isEmpty()
|
||
)->values();
|
||
}
|
||
|
||
// ── 角色列表 (僅 data-config Tab 版需要) ─────────────────────
|
||
$paginated_roles = collect();
|
||
$all_permissions = collect();
|
||
|
||
if ($isSubAccountRoute) {
|
||
$rolesQuery = \App\Models\System\Role::query()->with(['permissions', 'users', 'company'])->visibleTo($user);
|
||
if ($search = $request->input('roles_search')) {
|
||
$rolesQuery->where('name', 'like', "%{$search}%");
|
||
}
|
||
if ($user->isSystemAdmin() && $request->filled('roles_company_id')) {
|
||
$cid = $request->input('roles_company_id');
|
||
$cid === 'system' ? $rolesQuery->where('is_system', true) : $rolesQuery->where('company_id', $cid);
|
||
}
|
||
$roles_per_page = $request->input('roles_per_page', 10);
|
||
$paginated_roles = $rolesQuery->latest()->paginate($roles_per_page, ['*'], 'roles_page')->withQueryString();
|
||
|
||
$permissionQuery = \Spatie\Permission\Models\Permission::query();
|
||
if (!$user->isSystemAdmin()) {
|
||
$permissionQuery->whereIn('name', $user->getAllPermissions()->pluck('name'));
|
||
}
|
||
$all_permissions = $permissionQuery->get()
|
||
->reject(fn($p) => $p->name === 'menu.data-config.sub-account-roles')
|
||
->groupBy(fn($p) => str_starts_with($p->name, 'menu.') ? 'menu' : 'other');
|
||
}
|
||
|
||
$title = $isSubAccountRoute ? __('Sub Account Management') : __('Account Management');
|
||
$view = $isSubAccountRoute ? 'admin.data-config.accounts' : 'admin.permission.accounts';
|
||
|
||
return view($view, compact(
|
||
'users', 'companies', 'roles', 'paginated_roles', 'all_permissions', 'title', 'tab', 'currentUserRoleIds'
|
||
));
|
||
}
|
||
|
||
/**
|
||
* Store a newly created account in storage.
|
||
*/
|
||
public function storeAccount(Request $request)
|
||
{
|
||
$validated = $request->validate([
|
||
'name' => 'required|string|max:255',
|
||
'username' => 'required|string|max:255|unique:users,username',
|
||
'email' => 'nullable|email|max:255|unique:users,email',
|
||
'password' => 'required|string|min:8',
|
||
'role' => 'required|string',
|
||
'status' => 'required|boolean',
|
||
'company_id' => 'nullable|exists:companies,id',
|
||
'phone' => 'nullable|string|max:20',
|
||
]);
|
||
|
||
// 深度上限:level >= MAX_LEVEL 的子帳號不可再建立下層(系統管理員不受限)。
|
||
// 此為後端硬擋,獨立於權限判斷(即使持有子帳號管理權限也不得突破層級)。
|
||
if (!auth()->user()->canCreateSubAccount()) {
|
||
return redirect()->back()->with('error', __('Sub-accounts at this level cannot create further sub-accounts.'));
|
||
}
|
||
|
||
$company_id = auth()->user()->isSystemAdmin() ? ($validated['company_id'] ?? null) : auth()->user()->company_id;
|
||
|
||
// 查找角色:優先尋找該公司的角色,若無則尋找全域範本
|
||
$role = \App\Models\System\Role::where('name', $validated['role'])
|
||
->where(function($q) use ($company_id) {
|
||
$q->where('company_id', $company_id)->orWhereNull('company_id');
|
||
})
|
||
->first();
|
||
|
||
if (!$role) {
|
||
return redirect()->back()->with('error', __('Role not found.'));
|
||
}
|
||
|
||
// 驗證角色與公司的匹配性 (RBAC Safeguard)
|
||
if ($company_id !== null) {
|
||
// 如果是租戶帳號,絕對不能指派超級管理員角色 (super-admin)
|
||
if ($role->name === 'super-admin') {
|
||
return redirect()->back()->with('error', __('Super-admin role cannot be assigned to tenant accounts.'));
|
||
}
|
||
// 如果角色有特定的 company_id,必須匹配
|
||
if ($role->company_id !== null && $role->company_id != $company_id) {
|
||
return redirect()->back()->with('error', __('This role belongs to another company and cannot be assigned.'));
|
||
}
|
||
} else {
|
||
// 如果是系統層級帳號,只能選全域系統角色 (is_system = 1)
|
||
if (!$role->is_system) {
|
||
return redirect()->back()->with('error', __('Only system roles can be assigned to platform administrative accounts.'));
|
||
}
|
||
}
|
||
|
||
// 角色初始化與克隆邏輯 (只有 super-admin 在幫空白公司開帳號時觸發)
|
||
$company_id = auth()->user()->isSystemAdmin() ? ($validated['company_id'] ?? null) : auth()->user()->company_id;
|
||
|
||
if ($company_id && $role && $role->company_id === null && $role->name !== 'super-admin') {
|
||
// 檢查該公司是否已有名為「管理員」的角色
|
||
$existingRole = \App\Models\System\Role::where('company_id', $company_id)
|
||
->where('name', '管理員')
|
||
->first();
|
||
|
||
if (!$existingRole) {
|
||
// 克隆範本為該公司的「管理員」
|
||
$newRole = \App\Models\System\Role::query()->create([
|
||
'name' => '管理員',
|
||
'guard_name' => 'web',
|
||
'company_id' => $company_id,
|
||
'is_system' => false,
|
||
'is_company_admin' => true,
|
||
]);
|
||
$newRole->syncPermissions($role->getPermissionNames());
|
||
$role = $newRole;
|
||
} else {
|
||
// 如果已存在名為「管理員」的角色,則直接使用它
|
||
$role = $existingRole;
|
||
}
|
||
}
|
||
|
||
// 角色指派子集驗證:租戶只能指派「權限為自身子集」的角色,避免透過指派高權限角色造成權限提升。
|
||
if (!auth()->user()->isSystemAdmin()) {
|
||
$operatorPerms = auth()->user()->getAllPermissions()->pluck('name');
|
||
if (collect($role->getPermissionNames())->diff($operatorPerms)->isNotEmpty()) {
|
||
return redirect()->back()->with('error', __('You cannot assign a role with permissions you do not possess.'));
|
||
}
|
||
}
|
||
|
||
// 主帳號唯一性:一家公司只在「尚無主帳號」時,由系統管理員建立的第一個帳號自動成為主帳號 (is_admin)。
|
||
// 之後建立的帳號(含子帳號自建)一律 is_admin=false,確保每家公司恰有一個主帳號。
|
||
$creator = auth()->user();
|
||
$companyHasAdmin = $company_id
|
||
? \App\Models\System\User::where('company_id', $company_id)->where('is_admin', true)->exists()
|
||
: false;
|
||
$isMainAccount = $creator->isSystemAdmin() && !empty($company_id) && !$companyHasAdmin;
|
||
|
||
// 樹狀層級歸屬:
|
||
// - 主帳號:level 0、parent_id null。
|
||
// - 系統管理員建立的非主帳號:掛在該公司主帳號下、level 1。
|
||
// - 租戶自建:掛在建立者下、level = 建立者 level + 1。
|
||
if ($isMainAccount) {
|
||
$parentId = null;
|
||
$level = 0;
|
||
} elseif ($creator->isSystemAdmin()) {
|
||
$parentId = $company_id
|
||
? \App\Models\System\User::where('company_id', $company_id)->where('is_admin', true)->value('id')
|
||
: null;
|
||
$level = $parentId ? 1 : 0;
|
||
} else {
|
||
$parentId = $creator->id;
|
||
$level = $creator->level + 1;
|
||
}
|
||
|
||
$user = \App\Models\System\User::create([
|
||
'name' => $validated['name'],
|
||
'username' => $validated['username'],
|
||
'email' => $validated['email'],
|
||
'password' => \Illuminate\Support\Facades\Hash::make($validated['password']),
|
||
'status' => $validated['status'],
|
||
'company_id' => $company_id,
|
||
'parent_id' => $parentId,
|
||
'level' => $level,
|
||
'phone' => $validated['phone'] ?? null,
|
||
'is_admin' => $isMainAccount,
|
||
]);
|
||
|
||
$user->assignRole($role);
|
||
|
||
return redirect()->back()->with('success', __('Account created successfully.'));
|
||
}
|
||
|
||
/**
|
||
* Update the specified account in storage.
|
||
*/
|
||
public function updateAccount(Request $request, $id)
|
||
{
|
||
$user = \App\Models\System\User::findOrFail($id);
|
||
|
||
if ($user->hasRole('super-admin') && !auth()->user()->hasRole('super-admin')) {
|
||
return redirect()->back()->with('error', __('System super admin accounts can only be modified by other super admins.'));
|
||
}
|
||
|
||
// 層級越權防護:租戶只能管理可管轄範圍內的帳號(主帳號=同公司全部;子帳號=自己直接建立的下層)。
|
||
if (!auth()->user()->canManageAccount($user)) {
|
||
return redirect()->back()->with('error', __('You do not have permission to manage this account.'));
|
||
}
|
||
|
||
$validated = $request->validate([
|
||
'name' => 'required|string|max:255',
|
||
'username' => 'required|string|max:255|unique:users,username,' . $id,
|
||
'email' => 'nullable|email|max:255|unique:users,email,' . $id,
|
||
'password' => 'nullable|string|min:8',
|
||
'role' => 'required|string',
|
||
'status' => 'required|boolean',
|
||
'company_id' => 'nullable|exists:companies,id',
|
||
'phone' => 'nullable|string|max:20',
|
||
]);
|
||
|
||
$target_company_id = auth()->user()->isSystemAdmin() ? ($validated['company_id'] ?? null) : auth()->user()->company_id;
|
||
|
||
// 查找角色:優先尋找該公司的角色,若無則尋找全域範本
|
||
$roleObj = \App\Models\System\Role::where('name', $validated['role'])
|
||
->where(function($q) use ($target_company_id) {
|
||
$q->where('company_id', $target_company_id)->orWhereNull('company_id');
|
||
})
|
||
->first();
|
||
|
||
if (!$roleObj) {
|
||
return redirect()->back()->with('error', __('Role not found.'));
|
||
}
|
||
|
||
// 驗證角色與公司的匹配性 (RBAC Safeguard)
|
||
if ($user->id !== auth()->id()) { // 排除編輯自己 (super-admin 有特殊邏輯)
|
||
if ($target_company_id !== null) {
|
||
// 租戶層級排除 super-admin
|
||
if ($roleObj->name === 'super-admin') {
|
||
return redirect()->back()->with('error', __('Super-admin role cannot be assigned to tenant accounts.'));
|
||
}
|
||
if ($roleObj->company_id !== null && $roleObj->company_id != $target_company_id) {
|
||
return redirect()->back()->with('error', __('This role belongs to another company and cannot be assigned.'));
|
||
}
|
||
} else {
|
||
if (!$roleObj->is_system) {
|
||
return redirect()->back()->with('error', __('Only global system roles can be assigned to platform administrative accounts.'));
|
||
}
|
||
}
|
||
}
|
||
|
||
$updateData = [
|
||
'name' => $validated['name'],
|
||
'username' => $validated['username'],
|
||
'email' => $validated['email'],
|
||
'status' => $validated['status'],
|
||
'phone' => $validated['phone'] ?? null,
|
||
];
|
||
|
||
// 只有系統管理員在編輯租戶帳號時,且該帳號原本不是管理員,才可能觸發標記(視需求而定)
|
||
// 這裡我們維持 storeAccount 的邏輯:如果是系統管理員幫公司「開站」或「首配」,才自動標記
|
||
// 為求嚴謹,我們檢查該公司是否已經有 is_admin,如果沒有,當前這個人可以是第一個
|
||
if (auth()->user()->isSystemAdmin() && !empty($validated['company_id']) && !$user->is_admin) {
|
||
$hasAdmin = \App\Models\System\User::where('company_id', $validated['company_id'])
|
||
->where('is_admin', true)
|
||
->exists();
|
||
if (!$hasAdmin) {
|
||
$updateData['is_admin'] = true;
|
||
}
|
||
}
|
||
|
||
if (auth()->user()->isSystemAdmin()) {
|
||
// 防止超級管理員不小心把自己綁定到租客公司或降級
|
||
if ($user->id === auth()->id()) {
|
||
$updateData['company_id'] = null;
|
||
$validated['role'] = 'super-admin';
|
||
} else {
|
||
$updateData['company_id'] = $validated['company_id'];
|
||
}
|
||
}
|
||
|
||
if (!empty($validated['password'])) {
|
||
$updateData['password'] = \Illuminate\Support\Facades\Hash::make($validated['password']);
|
||
}
|
||
|
||
// 角色初始化與克隆邏輯
|
||
$target_company_id = auth()->user()->isSystemAdmin() ? ($validated['company_id'] ?? null) : auth()->user()->company_id;
|
||
|
||
if ($target_company_id && $roleObj && $roleObj->company_id === null && $roleObj->name !== 'super-admin') {
|
||
// 檢查該公司是否已有名為「管理員」的角色
|
||
$existingRole = \App\Models\System\Role::where('company_id', $target_company_id)
|
||
->where('name', '管理員')
|
||
->first();
|
||
|
||
if (!$existingRole) {
|
||
$newRole = \App\Models\System\Role::query()->create([
|
||
'name' => '管理員',
|
||
'guard_name' => 'web',
|
||
'company_id' => $target_company_id,
|
||
'is_system' => false,
|
||
'is_company_admin' => true,
|
||
]);
|
||
$newRole->syncPermissions($roleObj->getPermissionNames());
|
||
$roleObj = $newRole;
|
||
} else {
|
||
$roleObj = $existingRole;
|
||
}
|
||
}
|
||
|
||
// 角色指派子集驗證:租戶只能指派「權限為自身子集」的角色,避免權限提升。
|
||
if (!auth()->user()->isSystemAdmin()) {
|
||
$operatorPerms = auth()->user()->getAllPermissions()->pluck('name');
|
||
if (collect($roleObj->getPermissionNames())->diff($operatorPerms)->isNotEmpty()) {
|
||
return redirect()->back()->with('error', __('You cannot assign a role with permissions you do not possess.'));
|
||
}
|
||
}
|
||
|
||
$user->update($updateData);
|
||
|
||
// 如果是編輯自己且原本是超級管理員,強制保留 super-admin 角色
|
||
if ($user->id === auth()->id() && auth()->user()->isSystemAdmin()) {
|
||
$user->syncRoles(['super-admin']);
|
||
} else {
|
||
$user->syncRoles([$roleObj]);
|
||
}
|
||
|
||
return redirect()->back()->with('success', __('Account updated successfully.'));
|
||
}
|
||
|
||
/**
|
||
* Remove the specified account from storage.
|
||
*/
|
||
public function destroyAccount($id)
|
||
{
|
||
$user = \App\Models\System\User::findOrFail($id);
|
||
|
||
if ($user->hasRole('super-admin') && !auth()->user()->hasRole('super-admin')) {
|
||
return redirect()->back()->with('error', __('System super admin accounts can only be deleted by other super admins.'));
|
||
}
|
||
|
||
// 層級越權防護:租戶只能刪除可管轄範圍內的帳號。
|
||
if (!auth()->user()->canManageAccount($user)) {
|
||
return redirect()->back()->with('error', __('You do not have permission to manage this account.'));
|
||
}
|
||
|
||
if ($user->id === auth()->id()) {
|
||
return redirect()->back()->with('error', __('You cannot delete your own account.'));
|
||
}
|
||
|
||
// 主帳號保護:若該帳號是公司唯一主帳號,且公司仍有其他帳號,禁止刪除(須先轉移主帳號身分),
|
||
// 以維持「有帳號的公司恰有一個主帳號」的不變量。公司僅剩此主帳號時允許刪除(公司歸零)。
|
||
if ($user->is_admin && $user->company_id) {
|
||
$hasOtherAccounts = \App\Models\System\User::where('company_id', $user->company_id)
|
||
->where('id', '!=', $user->id)
|
||
->exists();
|
||
if ($hasOtherAccounts) {
|
||
return redirect()->back()->with('error', __('Cannot delete the company main account while other accounts exist. Please transfer the main account role first.'));
|
||
}
|
||
}
|
||
|
||
// 為了解決軟刪除導致的唯一索引佔用問題,刪除前先重命名唯一欄位
|
||
$timestamp = now()->getTimestamp();
|
||
$user->username = $user->username . '.deleted.' . $timestamp;
|
||
$user->email = $user->email . '.deleted.' . $timestamp;
|
||
$user->save();
|
||
|
||
$user->delete();
|
||
|
||
return redirect()->back()->with('success', __('Account deleted successfully.'));
|
||
}
|
||
|
||
public function toggleAccountStatus($id)
|
||
{
|
||
$user = \App\Models\System\User::findOrFail($id);
|
||
|
||
// 非超級管理員禁止切換 Super Admin 狀態
|
||
if ($user->hasRole('super-admin') && !auth()->user()->hasRole('super-admin')) {
|
||
return back()->with('error', __('Only Super Admins can change other Super Admin status.'));
|
||
}
|
||
|
||
// 層級越權防護:租戶只能切換可管轄範圍內的帳號狀態。
|
||
if (!auth()->user()->canManageAccount($user)) {
|
||
return back()->with('error', __('You do not have permission to manage this account.'));
|
||
}
|
||
|
||
$user->status = $user->status ? 0 : 1;
|
||
$user->save();
|
||
|
||
$statusText = $user->status ? __('Enabled') : __('Disabled');
|
||
return back()->with('success', __('Account :name status has been changed to :status.', ['name' => $user->name, 'status' => $statusText]));
|
||
}
|
||
|
||
/**
|
||
* 獲取過濾後的活動模組權限清單
|
||
*/
|
||
private function getFilteredPermissions($user)
|
||
{
|
||
$permissionQuery = \Spatie\Permission\Models\Permission::query();
|
||
if (!$user->isSystemAdmin()) {
|
||
$permissionQuery->whereIn('name', $user->getAllPermissions()->pluck('name'));
|
||
}
|
||
|
||
$activeModules = [
|
||
'menu.machines',
|
||
'menu.warehouses',
|
||
'menu.sales',
|
||
'menu.analysis',
|
||
'menu.data-config',
|
||
'menu.remote',
|
||
'menu.basic',
|
||
'menu.permissions',
|
||
];
|
||
|
||
return $permissionQuery->get()
|
||
->filter(function($p) use ($activeModules) {
|
||
if (str_starts_with($p->name, 'menu.')) {
|
||
foreach ($activeModules as $module) {
|
||
if ($p->name === $module || str_starts_with($p->name, $module . '.')) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
return true;
|
||
})
|
||
->reject(fn($p) => $p->name === 'menu.data-config.sub-account-roles')
|
||
->groupBy(function($perm) {
|
||
if (str_starts_with($perm->name, 'menu.')) {
|
||
return 'menu';
|
||
}
|
||
return 'other';
|
||
});
|
||
}
|
||
}
|