feat: 實作使用者啟停用功能與安全性強化
- 新增使用者「啟用/停用」狀態切換功能 (含後端 API、權限控管、活動紀錄) - 強化安全性:隱藏超級管理員角色的可見度與操作權限 - 更新開發規範:加入多租戶資料同步規範於 framework.md - 前端優化:使用 Switch 元件進行狀態快速切換,調整表格欄位順序
This commit is contained in:
@@ -43,6 +43,17 @@ class LoginController extends Controller
|
||||
$credentials = $request->only('username', 'password');
|
||||
|
||||
if (Auth::attempt($credentials, $request->boolean('remember'))) {
|
||||
// Check activation status
|
||||
if (!Auth::user()->is_active) {
|
||||
Auth::guard('web')->logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'username' => '此帳號已被停用,請聯繫管理員。',
|
||||
]);
|
||||
}
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
$centralDomains = config('tenancy.central_domains', []);
|
||||
|
||||
@@ -22,9 +22,26 @@ class UserController extends Controller
|
||||
$sortBy = $request->input('sort_by', 'id');
|
||||
$sortOrder = $request->input('sort_order', 'asc');
|
||||
$search = $request->input('search');
|
||||
$roleId = $request->input('role');
|
||||
|
||||
$query = User::with(['roles:id,name,display_name']);
|
||||
$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) {
|
||||
@@ -42,6 +59,11 @@ class UserController extends Controller
|
||||
});
|
||||
}
|
||||
|
||||
// 處理狀態篩選
|
||||
if ($isActive !== null && $isActive !== 'all') {
|
||||
$query->where('is_active', $isActive === '1' || $isActive === 'true');
|
||||
}
|
||||
|
||||
// 處理排序
|
||||
if (in_array($sortBy, ['name', 'created_at'])) {
|
||||
$query->orderBy($sortBy, $sortOrder);
|
||||
@@ -50,12 +72,19 @@ class UserController extends Controller
|
||||
}
|
||||
|
||||
$users = $query->paginate($perPage)->withQueryString();
|
||||
$roles = Role::select('id', 'name', 'display_name')->get();
|
||||
|
||||
// 只能看到自己權限以下的角色
|
||||
$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']),
|
||||
'filters' => $request->only(['per_page', 'sort_by', 'sort_order', 'search', 'role', 'is_active']),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -64,7 +93,11 @@ class UserController extends Controller
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$roles = Role::pluck('display_name', 'name');
|
||||
$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
|
||||
@@ -80,8 +113,10 @@ class UserController extends Controller
|
||||
'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 個字元',
|
||||
@@ -92,10 +127,16 @@ class UserController extends Controller
|
||||
'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' 紀錄以包含角色資訊
|
||||
@@ -123,7 +164,17 @@ class UserController extends Controller
|
||||
public function edit(string $id)
|
||||
{
|
||||
$user = User::with('roles')->findOrFail($id);
|
||||
$roles = Role::get(['id', 'name', 'display_name']);
|
||||
|
||||
// 安全檢查:非 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,
|
||||
@@ -139,12 +190,19 @@ class UserController extends Controller
|
||||
{
|
||||
$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' => '密碼確認不符',
|
||||
@@ -157,10 +215,6 @@ class UserController extends Controller
|
||||
'username' => $validated['username'],
|
||||
];
|
||||
|
||||
if (!empty($validated['password'])) {
|
||||
$userData['password'] = Hash::make($validated['password']);
|
||||
}
|
||||
|
||||
$user->fill($userData);
|
||||
|
||||
// 捕捉變更屬性以進行手動記錄
|
||||
@@ -179,6 +233,11 @@ class UserController extends Controller
|
||||
// 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(', ');
|
||||
@@ -230,6 +289,11 @@ class UserController extends Controller
|
||||
{
|
||||
$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', '無法刪除超級管理員帳號');
|
||||
}
|
||||
@@ -240,6 +304,46 @@ class UserController extends Controller
|
||||
|
||||
$user->delete();
|
||||
|
||||
return redirect()->route('users.index')->with('success', '使用者已刪除');
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ class User extends Authenticatable
|
||||
'email',
|
||||
'username',
|
||||
'password',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -56,7 +57,9 @@ class User extends Authenticatable
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
|
||||
'password' => 'hashed',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ Route::middleware('auth')->group(function () {
|
||||
});
|
||||
Route::get('/users/{user}/edit', [UserController::class, 'edit'])->middleware('permission:users.edit')->name('users.edit');
|
||||
Route::put('/users/{user}', [UserController::class, 'update'])->middleware('permission:users.edit')->name('users.update');
|
||||
Route::patch('/users/{user}/toggle-active', [UserController::class, 'toggleActive'])->middleware('permission:users.activate')->name('users.toggle-active');
|
||||
Route::delete('/users/{user}', [UserController::class, 'destroy'])->middleware('permission:users.delete')->name('users.destroy');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user