feat: [users] 슈퍼관리자 보호 기능 구현

- 일반관리자가 슈퍼관리자 수정/삭제 불가
- API Controller: update/destroy에서 403 반환
- Web Controller: edit에서 403 abort
- FormRequest: is_super_admin 필드 강제/유지 처리
- View: 테이블, 모달, 생성/수정 폼에서 버튼/체크박스 숨김

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-30 23:05:07 +09:00
parent 6be0a219c3
commit 049fa7ed61
8 changed files with 88 additions and 11 deletions

View File

@@ -93,6 +93,15 @@ public function store(StoreUserRequest $request): JsonResponse
public function update(UpdateUserRequest $request, int $id): JsonResponse
{
try {
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자를 수정하려는 경우 차단
$targetUser = $this->userService->getUserById($id);
if ($targetUser?->is_super_admin && ! auth()->user()?->is_super_admin) {
return response()->json([
'success' => false,
'message' => '슈퍼관리자는 수정할 수 없습니다.',
], 403);
}
$result = $this->userService->updateUser($id, $request->validated());
if (! $result) {
@@ -121,6 +130,15 @@ public function update(UpdateUserRequest $request, int $id): JsonResponse
public function destroy(int $id): JsonResponse
{
try {
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자를 삭제하려는 경우 차단
$targetUser = $this->userService->getUserById($id);
if ($targetUser?->is_super_admin && ! auth()->user()?->is_super_admin) {
return response()->json([
'success' => false,
'message' => '슈퍼관리자는 삭제할 수 없습니다.',
], 403);
}
$result = $this->userService->deleteUser($id);
if (! $result) {

View File

@@ -47,6 +47,11 @@ public function edit(int $id): View
abort(404, '사용자를 찾을 수 없습니다.');
}
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자를 수정하려는 경우 차단
if ($user->is_super_admin && ! auth()->user()?->is_super_admin) {
abort(403, '슈퍼관리자는 수정할 수 없습니다.');
}
$tenantId = session('selected_tenant_id');
// 역할/부서 목록 (테넌트별)

View File

@@ -14,6 +14,20 @@ public function authorize(): bool
return true;
}
/**
* Prepare the data for validation.
* 일반관리자가 슈퍼관리자를 생성하려는 경우 is_super_admin 필드 제거
*/
protected function prepareForValidation(): void
{
// 슈퍼관리자가 아닌 경우 is_super_admin 필드를 false로 강제 설정
if (! auth()->user()?->is_super_admin) {
$this->merge([
'is_super_admin' => false,
]);
}
}
/**
* Get the validation rules that apply to the request.
*

View File

@@ -15,6 +15,27 @@ public function authorize(): bool
return true;
}
/**
* Prepare the data for validation.
* 일반관리자가 슈퍼관리자 관련 필드를 변경하려는 경우 처리
*/
protected function prepareForValidation(): void
{
$userId = $this->route('id');
$targetUser = \App\Models\User::find($userId);
$currentUser = auth()->user();
// 슈퍼관리자가 아닌 경우
if (! $currentUser?->is_super_admin) {
// is_super_admin 필드가 있으면 제거 (기존 값 유지)
if ($this->has('is_super_admin')) {
$this->merge([
'is_super_admin' => $targetUser?->is_super_admin ?? false,
]);
}
}
}
/**
* Get the validation rules that apply to the request.
*

View File

@@ -139,11 +139,13 @@ class="h-4 w-4 text-green-600 rounded focus:ring-2 focus:ring-green-500">
class="h-4 w-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500">
<span class="ml-2 text-sm text-gray-700">활성 상태</span>
</label>
@if(auth()->user()?->is_super_admin)
<label class="flex items-center">
<input type="checkbox" name="is_super_admin" value="1"
class="h-4 w-4 text-red-600 rounded focus:ring-2 focus:ring-red-500">
<span class="ml-2 text-sm text-gray-700">슈퍼 관리자</span>
</label>
@endif
</div>
</div>

View File

@@ -144,12 +144,14 @@ class="h-4 w-4 text-green-600 rounded focus:ring-2 focus:ring-green-500">
class="h-4 w-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500">
<span class="ml-2 text-sm text-gray-700">활성 상태</span>
</label>
@if(auth()->user()?->is_super_admin)
<label class="flex items-center">
<input type="checkbox" name="is_super_admin" value="1"
{{ old('is_super_admin', $user->is_super_admin) ? 'checked' : '' }}
class="h-4 w-4 text-red-600 rounded focus:ring-2 focus:ring-red-500">
<span class="ml-2 text-sm text-gray-700">슈퍼 관리자</span>
</label>
@endif
</div>
</div>

View File

@@ -179,23 +179,31 @@ class="text-xs text-blue-600 hover:text-blue-800 hover:underline">
</div>
{{-- 하단 버튼 --}}
@php
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자를 수정/삭제할 수 없음
$canModify = ! $user->is_super_admin || auth()->user()?->is_super_admin;
@endphp
<div class="flex justify-end gap-2 mt-6 pt-4 border-t border-gray-200">
<button type="button"
onclick="UserModal.close()"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">
닫기
</button>
@if(!$user->deleted_at)
@if($canModify)
@if(!$user->deleted_at)
<button type="button"
onclick="UserModal.deleteUser()"
class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-700">
삭제
</button>
@endif
<button type="button"
onclick="UserModal.deleteUser()"
class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-700">
삭제
onclick="UserModal.goToEdit()"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">
수정
</button>
@else
<span class="px-4 py-2 text-sm text-gray-400">슈퍼관리자는 수정할 없습니다</span>
@endif
<button type="button"
onclick="UserModal.goToEdit()"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">
수정
</button>
</div>
</div>

View File

@@ -78,6 +78,10 @@
@endif
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium" onclick="event.stopPropagation()">
@php
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자를 수정/삭제할 수 없음
$canModify = ! $user->is_super_admin || auth()->user()?->is_super_admin;
@endphp
@if($user->deleted_at)
<!-- 삭제된 항목 - 슈퍼관리자만 복구/영구삭제 가능 -->
@if(auth()->user()?->is_super_admin)
@@ -92,8 +96,8 @@ class="text-red-600 hover:text-red-900">
@else
<span class="text-gray-400 text-xs">삭제됨</span>
@endif
@else
<!-- 활성 항목 -->
@elseif($canModify)
<!-- 활성 항목 (수정 가능한 경우만) -->
<a href="{{ route('users.edit', $user->id) }}"
onclick="event.stopPropagation()"
class="text-blue-600 hover:text-blue-900 mr-3">
@@ -102,6 +106,9 @@ class="text-blue-600 hover:text-blue-900 mr-3">
<button onclick="confirmDelete({{ $user->id }}, '{{ $user->name }}')" class="text-red-600 hover:text-red-900">
삭제
</button>
@else
<!-- 슈퍼관리자 - 일반관리자는 수정/삭제 불가 -->
<span class="text-gray-400 text-xs">수정 불가</span>
@endif
</td>
</tr>