Files
star-erp/app/Modules/Core/Controllers/UserController.php
sky121113 d671c08338
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 1m1s
feat: 實作使用者啟停用功能與安全性強化
- 新增使用者「啟用/停用」狀態切換功能 (含後端 API、權限控管、活動紀錄)
- 強化安全性:隱藏超級管理員角色的可見度與操作權限
- 更新開發規範:加入多租戶資料同步規範於 framework.md
- 前端優化:使用 Switch 元件進行狀態快速切換,調整表格欄位順序
2026-02-03 11:51:46 +08:00

350 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Modules\Core\Controllers;
use App\Http\Controllers\Controller;
use App\Modules\Core\Models\User;
use Illuminate\Http\Request;
use Spatie\Permission\Models\Role;
use Inertia\Inertia;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Hash;
class UserController extends Controller
{
/**
* 顯示資源列表。
*/
public function index(Request $request)
{
$perPage = $request->input('per_page', 10);
$sortBy = $request->input('sort_by', 'id');
$sortOrder = $request->input('sort_order', 'asc');
$search = $request->input('search');
$roleId = $request->input('role');
$isActive = $request->input('is_active'); // 'all', '1', '0'
$query = User::query();
// 隱藏超級管理員:若非 super-admin則不可看到 super-admin 過往
if (!auth()->user()->hasRole('super-admin')) {
$query->whereDoesntHave('roles', function ($q) {
$q->where('name', 'super-admin');
});
// 預載入角色時也過濾掉 super-admin 標籤
$query->with(['roles' => function ($q) {
$q->select('id', 'name', 'display_name')
->where('name', '!=', 'super-admin');
}]);
} else {
$query->with(['roles:id,name,display_name']);
}
// 處理搜尋
if ($search) {
$query->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%")
->orWhere('username', 'like', "%{$search}%");
});
}
// 處理角色篩選
if ($roleId && $roleId !== 'all') {
$query->whereHas('roles', function ($q) use ($roleId) {
$q->where('id', $roleId);
});
}
// 處理狀態篩選
if ($isActive !== null && $isActive !== 'all') {
$query->where('is_active', $isActive === '1' || $isActive === 'true');
}
// 處理排序
if (in_array($sortBy, ['name', 'created_at'])) {
$query->orderBy($sortBy, $sortOrder);
} else {
$query->orderBy('id', 'asc');
}
$users = $query->paginate($perPage)->withQueryString();
// 只能看到自己權限以下的角色
$rolesQuery = Role::select('id', 'name', 'display_name');
if (!auth()->user()->hasRole('super-admin')) {
$rolesQuery->where('name', '!=', 'super-admin');
}
$roles = $rolesQuery->get();
return Inertia::render('Admin/User/Index', [
'users' => $users,
'users' => $users,
'roles' => $roles,
'filters' => $request->only(['per_page', 'sort_by', 'sort_order', 'search', 'role', 'is_active']),
]);
}
/**
* 顯示建立新資源的表單。
*/
public function create()
{
$rolesQuery = Role::query();
if (!auth()->user()->hasRole('super-admin')) {
$rolesQuery->where('name', '!=', 'super-admin');
}
$roles = $rolesQuery->pluck('display_name', 'name');
return Inertia::render('Admin/User/Create', [
'roles' => $roles
]);
}
/**
* 將新建立的資源儲存到儲存體中。
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['nullable', 'string', 'email', 'max:255', 'unique:users'],
'username' => ['required', 'string', 'max:255', 'unique:users'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
'roles' => ['array'],
'is_active' => ['boolean'],
], [
'password.required' => '請輸入密碼',
'password.min' => '密碼長度至少需 :min 個字元',
'password.confirmed' => '密碼確認不符',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'username' => $validated['username'],
'password' => Hash::make($validated['password']),
'is_active' => $request->boolean('is_active', true),
]);
if (!empty($validated['roles'])) {
// 安全檢查:非 super-admin 不能賦予 super-admin 角色
if (!auth()->user()->hasRole('super-admin') && in_array('super-admin', $validated['roles'])) {
abort(403, '您沒有權限指派系統管理員角色');
}
$user->syncRoles($validated['roles']);
// 更新 'created' 紀錄以包含角色資訊
$activity = \Spatie\Activitylog\Models\Activity::where('subject_type', get_class($user))
->where('subject_id', $user->id)
->where('event', 'created')
->latest()
->first();
if ($activity) {
$roleNames = $user->roles()->pluck('display_name')->join(', ');
$properties = $activity->properties->toArray();
$properties['attributes']['role_id'] = $roleNames;
$activity->properties = $properties;
$activity->save();
}
}
return redirect()->route('users.index')->with('success', '使用者建立成功');
}
/**
* 顯示編輯指定資源的表單。
*/
public function edit(string $id)
{
$user = User::with('roles')->findOrFail($id);
// 安全檢查:非 super-admin 不能編輯 super-admin
if ($user->hasRole('super-admin') && !auth()->user()->hasRole('super-admin')) {
abort(403, '您沒有權限編輯系統管理員');
}
$rolesQuery = Role::select('id', 'name', 'display_name');
if (!auth()->user()->hasRole('super-admin')) {
$rolesQuery->where('name', '!=', 'super-admin');
}
$roles = $rolesQuery->get();
return Inertia::render('Admin/User/Edit', [
'user' => $user,
'roles' => $roles,
'currentRoles' => $user->getRoleNames()
]);
}
/**
* 更新儲存體中的指定資源。
*/
public function update(Request $request, string $id)
{
$user = User::findOrFail($id);
// 安全檢查:非 super-admin 不能更新 super-admin
if ($user->hasRole('super-admin') && !auth()->user()->hasRole('super-admin')) {
abort(403, '您沒有權限編輯系統管理員');
}
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['nullable', 'string', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
'username' => ['required', 'string', 'max:255', Rule::unique('users')->ignore($user->id)],
'password' => ['nullable', 'string', 'min:8', 'confirmed'],
'roles' => ['array'],
'is_active' => ['boolean'],
], [
'password.min' => '密碼長度至少需 :min 個字元',
'password.confirmed' => '密碼確認不符',
]);
// 1. 準備資料並偵測變更
$userData = [
'name' => $validated['name'],
'email' => $validated['email'],
'username' => $validated['username'],
];
$user->fill($userData);
// 捕捉變更屬性以進行手動記錄
$dirty = $user->getDirty();
$oldAttributes = [];
$newAttributes = [];
foreach ($dirty as $key => $value) {
$oldAttributes[$key] = $user->getOriginal($key);
$newAttributes[$key] = $value;
}
// 儲存但不觸發事件(防止重複記錄)
$user->saveQuietly();
// 2. 處理角色
$roleChanges = null;
if (isset($validated['roles'])) {
// 安全檢查:非 super-admin 不能賦予 super-admin 角色
if (!auth()->user()->hasRole('super-admin') && in_array('super-admin', $validated['roles'])) {
abort(403, '您沒有權限指派系統管理員角色');
}
$oldRoles = $user->roles()->pluck('display_name')->join(', ');
$user->syncRoles($validated['roles']);
$newRoles = $user->roles()->pluck('display_name')->join(', ');
if ($oldRoles !== $newRoles) {
$roleChanges = [
'old' => $oldRoles,
'new' => $newRoles
];
}
}
// 3. 手動記錄活動(單一整合記錄)
if (!empty($newAttributes) || $roleChanges) {
$properties = [
'attributes' => $newAttributes,
'old' => $oldAttributes,
];
if ($roleChanges) {
$properties['attributes']['role_id'] = $roleChanges['new'];
$properties['old']['role_id'] = $roleChanges['old'];
}
activity()
->performedOn($user)
->causedBy(auth()->user())
->event('updated')
->withProperties($properties)
->tap(function (\Spatie\Activitylog\Contracts\Activity $activity) use ($user) {
// 手動加入快照,因為使用 saveQuietly 所以不使用模型的 LogOptions
$activity->properties = $activity->properties->merge([
'snapshot' => [
'name' => $user->name,
'username' => $user->username,
]
]);
})
->log('updated');
}
return redirect()->route('users.index')->with('success', '使用者更新成功');
}
/**
* 從儲存體中移除指定資源。
*/
public function destroy(string $id)
{
$user = User::findOrFail($id);
// 安全檢查:非 super-admin 不能刪除 super-admin
if ($user->hasRole('super-admin') && !auth()->user()->hasRole('super-admin')) {
abort(403, '您沒有權限刪除系統管理員');
}
if ($user->hasRole('super-admin')) {
return back()->with('error', '無法刪除超級管理員帳號');
}
if ($user->id === auth()->id()) {
return back()->with('error', '無法刪除自己');
}
$user->delete();
return redirect()->route('users.index')->with('success', "使用者「{$user->name}」已刪除");
}
/**
* 切換使用者啟用/停用狀態
*/
public function toggleActive(string $id)
{
$user = User::findOrFail($id);
// 安全檢查:不能停用自己
if ($user->id === auth()->id() && $user->is_active) {
return back()->with('error', '無法停用自己的帳號');
}
// 安全檢查:非 super-admin 不能停用 super-admin
if ($user->hasRole('super-admin') && !auth()->user()->hasRole('super-admin')) {
abort(403, '您沒有權限變更系統管理員狀態');
}
$oldStatus = $user->is_active;
$user->is_active = !$oldStatus;
$user->save();
// 記錄活動
activity()
->performedOn($user)
->causedBy(auth()->user())
->event('updated')
->withProperties([
'attributes' => ['is_active' => $user->is_active],
'old' => ['is_active' => $oldStatus],
'snapshot' => [
'name' => $user->name,
'username' => $user->username,
]
])
->log('updated');
$statusText = $user->is_active ? '已啟用' : '已停用';
return back()->with('success', "使用者「{$user->name}{$statusText}");
}
}