- 新增使用者「啟用/停用」狀態切換功能 (含後端 API、權限控管、活動紀錄) - 強化安全性:隱藏超級管理員角色的可見度與操作權限 - 更新開發規範:加入多租戶資料同步規範於 framework.md - 前端優化:使用 Switch 元件進行狀態快速切換,調整表格欄位順序
350 lines
12 KiB
PHP
350 lines
12 KiB
PHP
<?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}");
|
||
}
|
||
}
|