Files

350 lines
12 KiB
PHP
Raw Permalink Normal View History

<?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}");
}
}