824a3100ce
이번 PR 은 invyone 멀티테넌시 SaaS 의 "관리 plane vs 테넌트 plane" 격리를 4 영역(PR #A~D) 에서 강화하고, 별도로 진행 중이던 부서관리 후속 작업을 포함한다. # 보안 (plane 격리) PR #A — controller/CompanyManagementController 인증 누락 패치 /api/company-management/* 가 JWT/role/host 체크 없이 외부에서 누구나 회사 삭제 + 디스크 통계 호출 가능했던 critical 누수 막음. SuperAdminGuard.enforce() 적용. PR #C — cross-tenant 컨트롤러 호스트 격리 + 감사 로그 CrossTenantContext.requireManagementHost() 헬퍼 추가, 5 컨트롤러 (CrossTenantContext/Controller/UserController/RoleController/DeptController) 모두 테넌트 호스트에서 호출 시 403. CompanyAuditLogService 에 cross-tenant write 4종 (USER_CREATE/DELETE, PW_RESET, ROLE_UPDATE) audit action 추가. SuperAdminGuard.isTenantHost 가시성 public static 으로 승격. PR #B — 프론트 솔루션 전용 admin 페이지 가드 admin/* 페이지 전수 분류 결과 솔루션 전용 3건 식별: subdomainList / companyList / audit-log. 각 페이지에 isManagementHost useEffect 가드 + redirect 추가. 사이드바도 같이 숨김. PR #D — MENU_INFO.IS_SOLUTION_ONLY 컬럼 + DB-driven 메뉴 필터 V023 마이그레이션으로 컬럼 추가 + 솔루션 메뉴 3개 마킹. admin.xml selectUserMenuList 에 호스트 기반 필터 추가, AdminController.getUserMenus 가 Host 헤더로 is_management_host 결정. 프론트 MANAGEMENT_ONLY_MENU_URLS 하드코딩 set 폐기 (DB 가 대신함). 페이지 자체 가드는 defense in depth 로 유지. StartupSchemaMigrator 에 V023 등록되어 모든 테넌트 DB 부팅 시 자동 적용. # 부서관리 후속 (이전 PR #18/#19 follow-up) DepartmentController/Service + frontend deptMngList/department.ts 의 추가 작업분. 이번 격리 작업과 무관하지만 같이 정리. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
95 lines
3.5 KiB
Java
95 lines
3.5 KiB
Java
package com.erp.controller;
|
|
|
|
import com.erp.dto.ApiResponse;
|
|
import com.erp.provisioning.SuperAdminGuard;
|
|
import com.erp.service.CompanyManagementService;
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.http.ResponseEntity;
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
|
|
@RestController
|
|
@RequestMapping("/api/company-management")
|
|
@RequiredArgsConstructor
|
|
@Slf4j
|
|
public class CompanyManagementController {
|
|
|
|
private final SuperAdminGuard guard;
|
|
private final CompanyManagementService companyManagementService;
|
|
|
|
/**
|
|
* DELETE /api/company-management/:companyCode
|
|
* 회사 삭제 및 파일 정리 (soft delete)
|
|
*/
|
|
@DeleteMapping("/{companyCode}")
|
|
public ResponseEntity<ApiResponse<Map<String, Object>>> deleteCompany(
|
|
HttpServletRequest request,
|
|
@PathVariable String companyCode,
|
|
@RequestBody(required = false) Map<String, Object> body) {
|
|
|
|
guard.enforce(request);
|
|
|
|
Map<String, Object> params = new HashMap<>();
|
|
params.put("company_code", companyCode);
|
|
if (body != null) {
|
|
params.putAll(body);
|
|
}
|
|
|
|
try {
|
|
Map<String, Object> data = companyManagementService.deleteCompany(params);
|
|
String companyName = (String) data.get("company_name");
|
|
return ResponseEntity.ok(
|
|
ApiResponse.success(data, "회사 '" + companyName + "'이(가) 성공적으로 삭제되었습니다."));
|
|
} catch (IllegalArgumentException e) {
|
|
return ResponseEntity.status(404).body(ApiResponse.error(e.getMessage()));
|
|
} catch (RuntimeException e) {
|
|
log.error("회사 삭제 실패: companyCode={}", companyCode, e);
|
|
return ResponseEntity.status(500).body(ApiResponse.error(e.getMessage()));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/company-management/disk-usage/all
|
|
* 전체 회사 디스크 사용량 조회
|
|
* ※ /{companyCode}/disk-usage 보다 먼저 정의 (경로 특이성으로 충돌 없음)
|
|
*/
|
|
@GetMapping("/disk-usage/all")
|
|
public ResponseEntity<ApiResponse<Map<String, Object>>> getAllCompaniesDiskUsage(
|
|
HttpServletRequest request) {
|
|
|
|
guard.enforce(request);
|
|
|
|
try {
|
|
Map<String, Object> data = companyManagementService.getAllCompaniesDiskUsage();
|
|
return ResponseEntity.ok(ApiResponse.success(data));
|
|
} catch (Exception e) {
|
|
log.error("전체 디스크 사용량 조회 실패", e);
|
|
return ResponseEntity.status(500).body(ApiResponse.error("전체 디스크 사용량 조회 중 오류가 발생했습니다."));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/company-management/:companyCode/disk-usage
|
|
* 회사별 디스크 사용량 조회
|
|
*/
|
|
@GetMapping("/{companyCode}/disk-usage")
|
|
public ResponseEntity<ApiResponse<Map<String, Object>>> getCompanyDiskUsage(
|
|
HttpServletRequest request,
|
|
@PathVariable String companyCode) {
|
|
|
|
guard.enforce(request);
|
|
|
|
try {
|
|
Map<String, Object> data = companyManagementService.getCompanyDiskUsage(companyCode);
|
|
return ResponseEntity.ok(ApiResponse.success(data));
|
|
} catch (Exception e) {
|
|
log.error("디스크 사용량 조회 실패: companyCode={}", companyCode, e);
|
|
return ResponseEntity.status(500).body(ApiResponse.error("디스크 사용량 조회 중 오류가 발생했습니다."));
|
|
}
|
|
}
|
|
}
|