fix: [users] 슈퍼관리자 보호 기능 복원 라우트 수정
- routes/api.php: 8개 엔티티의 restore 라우트를 super.admin 미들웨어 밖으로 이동 - tenants, departments, users, menus, boards - pm/projects, pm/tasks, pm/issues - UserService.canAccessUser(): withTrashed() 적용하여 soft-deleted 사용자 권한 체크 가능 - UserPermissionService.canModifyUser(): withTrashed() 적용 (일관성 유지) 권한 정책: - 복원 (Restore): 일반관리자 가능 - 영구삭제 (Force Delete): 슈퍼관리자 전용 버그 수정: - 302 Found 에러 해결 (미들웨어 블로킹) - soft-deleted 사용자 복원 시 권한 체크 실패 해결 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,14 @@ public function index(Request $request): JsonResponse
|
||||
*/
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자 정보 조회 불가
|
||||
if (! $this->userService->canAccessUser($id)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '사용자를 찾을 수 없습니다.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$user = $this->userService->getUserById($id);
|
||||
|
||||
if (! $user) {
|
||||
@@ -165,6 +173,14 @@ public function destroy(int $id): JsonResponse
|
||||
*/
|
||||
public function restore(Request $request, int $id): JsonResponse
|
||||
{
|
||||
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자 복원 불가 (존재하지 않는 것처럼)
|
||||
if (! $this->userService->canAccessUser($id)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '사용자를 찾을 수 없습니다.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$this->userService->restoreUser($id);
|
||||
|
||||
// HTMX 요청 시 테이블 새로고침 트리거
|
||||
@@ -187,6 +203,14 @@ public function restore(Request $request, int $id): JsonResponse
|
||||
*/
|
||||
public function modal(Request $request, int $id): JsonResponse
|
||||
{
|
||||
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자 정보 조회 불가
|
||||
if (! $this->userService->canAccessUser($id)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '사용자를 찾을 수 없습니다.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$user = $this->userService->getUserForModal($id);
|
||||
|
||||
if (! $user) {
|
||||
|
||||
@@ -62,6 +62,14 @@ public function getMatrix(Request $request)
|
||||
return view('user-permissions.partials.empty-state');
|
||||
}
|
||||
|
||||
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자 권한 조회 불가
|
||||
if (! $this->userPermissionService->canModifyUser($userId)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '슈퍼관리자의 권한은 조회할 수 없습니다.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
// 사용자의 tenant_id로 메뉴 필터링
|
||||
$tenantId = $this->getEffectiveTenantId($request, $userId);
|
||||
|
||||
@@ -89,6 +97,14 @@ public function toggle(Request $request)
|
||||
$guardName = $this->getValidatedGuardName($request);
|
||||
$tenantId = $this->getEffectiveTenantId($request, $userId);
|
||||
|
||||
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자 권한 수정 불가
|
||||
if (! $this->userPermissionService->canModifyUser($userId)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '슈퍼관리자의 권한은 수정할 수 없습니다.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$newValue = $this->userPermissionService->togglePermission(
|
||||
$userId,
|
||||
$menuId,
|
||||
@@ -117,6 +133,14 @@ public function allowAll(Request $request)
|
||||
$guardName = $this->getValidatedGuardName($request);
|
||||
$tenantId = $this->getEffectiveTenantId($request, $userId);
|
||||
|
||||
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자 권한 수정 불가
|
||||
if (! $this->userPermissionService->canModifyUser($userId)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '슈퍼관리자의 권한은 수정할 수 없습니다.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$this->userPermissionService->allowAllPermissions($userId, $tenantId, $guardName);
|
||||
|
||||
// 전체 매트릭스 다시 로드
|
||||
@@ -139,6 +163,14 @@ public function denyAll(Request $request)
|
||||
$guardName = $this->getValidatedGuardName($request);
|
||||
$tenantId = $this->getEffectiveTenantId($request, $userId);
|
||||
|
||||
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자 권한 수정 불가
|
||||
if (! $this->userPermissionService->canModifyUser($userId)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '슈퍼관리자의 권한은 수정할 수 없습니다.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$this->userPermissionService->denyAllPermissions($userId, $tenantId, $guardName);
|
||||
|
||||
// 전체 매트릭스 다시 로드
|
||||
@@ -161,6 +193,14 @@ public function reset(Request $request)
|
||||
$guardName = $this->getValidatedGuardName($request);
|
||||
$tenantId = $this->getEffectiveTenantId($request, $userId);
|
||||
|
||||
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자 권한 수정 불가
|
||||
if (! $this->userPermissionService->canModifyUser($userId)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '슈퍼관리자의 권한은 수정할 수 없습니다.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$this->userPermissionService->resetToDefaultPermissions($userId, $tenantId, $guardName);
|
||||
|
||||
// 전체 매트릭스 다시 로드
|
||||
|
||||
@@ -41,17 +41,17 @@ public function create(): View
|
||||
*/
|
||||
public function edit(int $id): View
|
||||
{
|
||||
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자에 접근 불가 (존재하지 않는 것처럼)
|
||||
if (! $this->userService->canAccessUser($id)) {
|
||||
abort(404, '사용자를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
$user = $this->userService->getUserById($id);
|
||||
|
||||
if (! $user) {
|
||||
abort(404, '사용자를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자를 수정하려는 경우 차단
|
||||
if ($user->is_super_admin && ! auth()->user()?->is_super_admin) {
|
||||
abort(403, '슈퍼관리자는 수정할 수 없습니다.');
|
||||
}
|
||||
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
// 역할/부서 목록 (테넌트별)
|
||||
|
||||
@@ -691,21 +691,49 @@ public function hasPermission(int $userId, int $menuId, string $permissionType,
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 대상 사용자가 슈퍼관리자인지 검증 (일반관리자는 슈퍼관리자 수정 불가)
|
||||
*
|
||||
* @param int $targetUserId 대상 사용자 ID
|
||||
* @return bool true면 수정 가능, false면 수정 불가
|
||||
*/
|
||||
public function canModifyUser(int $targetUserId): bool
|
||||
{
|
||||
// withTrashed()를 사용하여 일관성 유지
|
||||
$targetUser = User::withTrashed()->find($targetUserId);
|
||||
$currentUser = auth()->user();
|
||||
|
||||
// 대상 사용자가 슈퍼관리자이고 현재 사용자가 슈퍼관리자가 아니면 수정 불가
|
||||
if ($targetUser?->is_super_admin && ! $currentUser?->is_super_admin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트별 사용자 목록 조회 (권한 개수 포함)
|
||||
* 일반관리자는 슈퍼관리자가 목록에서 제외됨
|
||||
*
|
||||
* @param int $tenantId 테넌트 ID
|
||||
* @return \Illuminate\Support\Collection 사용자 목록
|
||||
*/
|
||||
public function getUsersByTenant(int $tenantId): \Illuminate\Support\Collection
|
||||
{
|
||||
$users = User::whereHas('tenants', function ($query) use ($tenantId) {
|
||||
$currentUser = auth()->user();
|
||||
|
||||
$query = User::whereHas('tenants', function ($query) use ($tenantId) {
|
||||
$query->where('tenants.id', $tenantId)
|
||||
->where('user_tenants.is_active', true);
|
||||
})
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
->where('is_active', true);
|
||||
|
||||
// 일반관리자는 슈퍼관리자를 볼 수 없음
|
||||
if (! $currentUser?->is_super_admin) {
|
||||
$query->where('is_super_admin', false);
|
||||
}
|
||||
|
||||
$users = $query->orderBy('name')->get();
|
||||
|
||||
// 각 사용자별 권한 개수 계산
|
||||
$now = now();
|
||||
|
||||
@@ -6,10 +6,15 @@
|
||||
use App\Models\User;
|
||||
use App\Models\UserRole;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class UserService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ArchiveService $archiveService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 사용자 목록 조회 (페이지네이션)
|
||||
*/
|
||||
@@ -18,6 +23,11 @@ public function getUsers(array $filters = [], int $perPage = 15): LengthAwarePag
|
||||
$tenantId = session('selected_tenant_id');
|
||||
$query = User::query()->withTrashed();
|
||||
|
||||
// 슈퍼관리자 보호: 일반관리자는 슈퍼관리자를 볼 수 없음
|
||||
if (! auth()->user()?->is_super_admin) {
|
||||
$query->where('is_super_admin', false);
|
||||
}
|
||||
|
||||
// 역할/부서 관계 eager loading (테넌트별)
|
||||
if ($tenantId) {
|
||||
$query->with([
|
||||
@@ -223,15 +233,25 @@ public function restoreUser(int $id): bool
|
||||
|
||||
/**
|
||||
* 사용자 영구 삭제 (슈퍼관리자 전용)
|
||||
*
|
||||
* 1. 사용자 데이터를 아카이브에 저장
|
||||
* 2. 관련 데이터 삭제
|
||||
* 3. 사용자 영구 삭제
|
||||
*/
|
||||
public function forceDeleteUser(int $id): bool
|
||||
{
|
||||
$user = User::withTrashed()->findOrFail($id);
|
||||
|
||||
// 관련 데이터 먼저 삭제
|
||||
$user->tenants()->detach(); // user_tenants 관계 삭제
|
||||
return DB::transaction(function () use ($user) {
|
||||
// 1. 아카이브에 저장 (복원 가능하도록)
|
||||
$this->archiveService->archiveUser($user);
|
||||
|
||||
return $user->forceDelete();
|
||||
// 2. 관련 데이터 삭제
|
||||
$user->tenants()->detach(); // user_tenants 관계 삭제
|
||||
|
||||
// 3. 사용자 영구 삭제
|
||||
return $user->forceDelete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -368,6 +388,11 @@ public function getActiveUsers()
|
||||
$tenantId = session('selected_tenant_id');
|
||||
$query = User::query()->where('is_active', true);
|
||||
|
||||
// 슈퍼관리자 보호: 일반관리자는 슈퍼관리자를 볼 수 없음
|
||||
if (! auth()->user()?->is_super_admin) {
|
||||
$query->where('is_super_admin', false);
|
||||
}
|
||||
|
||||
// 테넌트 필터링 (user_tenants pivot을 통한 필터링)
|
||||
if ($tenantId) {
|
||||
$query->whereHas('tenants', function ($q) use ($tenantId) {
|
||||
@@ -377,4 +402,24 @@ public function getActiveUsers()
|
||||
|
||||
return $query->orderBy('name')->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 슈퍼관리자 보호: 일반관리자가 슈퍼관리자에 접근할 수 있는지 확인
|
||||
*
|
||||
* @param int $targetUserId 대상 사용자 ID
|
||||
* @return bool true면 접근 가능, false면 접근 불가
|
||||
*/
|
||||
public function canAccessUser(int $targetUserId): bool
|
||||
{
|
||||
// withTrashed()를 사용하여 soft-deleted 사용자도 확인 (복원 시 필요)
|
||||
$targetUser = User::withTrashed()->find($targetUserId);
|
||||
$currentUser = auth()->user();
|
||||
|
||||
// 대상 사용자가 슈퍼관리자이고 현재 사용자가 슈퍼관리자가 아니면 접근 불가
|
||||
if ($targetUser?->is_super_admin && ! $currentUser?->is_super_admin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user