diff --git a/src/main/java/side/onetime/auth/dto/CustomAdminDetails.java b/src/main/java/side/onetime/auth/dto/CustomAdminDetails.java new file mode 100644 index 00000000..7b6f05de --- /dev/null +++ b/src/main/java/side/onetime/auth/dto/CustomAdminDetails.java @@ -0,0 +1,36 @@ +package side.onetime.auth.dto; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import side.onetime.domain.AdminUser; +import side.onetime.domain.enums.AdminStatus; + +import java.util.Collection; +import java.util.Collections; + +public record CustomAdminDetails(AdminUser admin) implements UserDetails { + + @Override + public Collection getAuthorities() { + if (admin.getAdminStatus() == AdminStatus.PENDING_APPROVAL) { + return Collections.singletonList(new SimpleGrantedAuthority("ROLE_PENDING_APPROVAL")); + } + + return Collections.singletonList(new SimpleGrantedAuthority("ROLE_ADMIN")); + } + + @Override + public String getPassword() { + return admin.getPassword(); + } + + @Override + public String getUsername() { + return admin.getName(); + } + + public Long getId() { + return admin.getId(); + } +} diff --git a/src/main/java/side/onetime/auth/dto/CustomUserDetails.java b/src/main/java/side/onetime/auth/dto/CustomUserDetails.java index 5ba96f26..9eca246a 100644 --- a/src/main/java/side/onetime/auth/dto/CustomUserDetails.java +++ b/src/main/java/side/onetime/auth/dto/CustomUserDetails.java @@ -1,16 +1,18 @@ package side.onetime.auth.dto; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import side.onetime.domain.User; import java.util.Collection; +import java.util.List; public record CustomUserDetails(User user) implements UserDetails { @Override public Collection getAuthorities() { - return null; + return List.of(new SimpleGrantedAuthority("ROLE_USER")); } @Override @@ -23,26 +25,6 @@ public String getUsername() { return user.getName(); } - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return true; - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return true; - } - public Long getId() { return user.getId(); } diff --git a/src/main/java/side/onetime/auth/exception/CustomAccessDeniedHandler.java b/src/main/java/side/onetime/auth/exception/CustomAccessDeniedHandler.java new file mode 100644 index 00000000..8b11d744 --- /dev/null +++ b/src/main/java/side/onetime/auth/exception/CustomAccessDeniedHandler.java @@ -0,0 +1,47 @@ +package side.onetime.auth.exception; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; +import side.onetime.global.common.status.ErrorStatus; + +import java.io.IOException; + +@Slf4j +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + + /** + * 접근할 수 있는 권한(Role)이 없을 때 발생한 accessDeniedException을 클라이언트에게 응답 형태로 반환합니다. + * + * @param request HTTP 요청 객체 + * @param response HTTP 응답 객체 + * @param accessDeniedException 접근 권한이 없어 발생한 exception + * @throws IOException 출력 스트림 처리 중 오류 발생 시 + */ + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String username = (authentication != null) ? authentication.getName() : "ANONYMOUS"; + log.error("❌ 금지된 접근 - 사용자: {}, 요청 URI: {}, 메서드: {}", username, request.getRequestURI(), request.getMethod()); + + ErrorStatus status = ErrorStatus._FORBIDDEN; + + response.setStatus(status.getHttpStatus().value()); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write( + "{" + + "\"is_success\": false," + + "\"code\": \"" + status.getCode() + "\"," + + "\"message\": \"" + status.getMessage() + "\"," + + "\"payload\": null" + + "}" + ); + } +} diff --git a/src/main/java/side/onetime/auth/exception/CustomAuthenticationEntryPoint.java b/src/main/java/side/onetime/auth/exception/CustomAuthenticationEntryPoint.java new file mode 100644 index 00000000..bc3c0232 --- /dev/null +++ b/src/main/java/side/onetime/auth/exception/CustomAuthenticationEntryPoint.java @@ -0,0 +1,43 @@ +package side.onetime.auth.exception; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; +import side.onetime.global.common.status.ErrorStatus; + +import java.io.IOException; + +@Slf4j +@Component +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + /** + * 로그인을 하지 않고 접근할 떄 발생한 authException을 클라이언트에게 응답 형태로 반환합니다. + * + * @param request HTTP 요청 객체 + * @param response HTTP 응답 객체 + * @param authException 로그인 없이 로그인이 필요한 리소스에 접근하여 발생한 exception + * @throws IOException 출력 스트림 처리 중 오류 발생 시 + */ + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + log.error("❌ 인증되지 않은 접근 - 요청 URI: {}, 메서드: {}", request.getRequestURI(), request.getMethod()); + + ErrorStatus status = ErrorStatus._UNAUTHORIZED; + + response.setStatus(status.getHttpStatus().value()); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write( + "{" + + "\"is_success\": false," + + "\"code\": \"" + status.getCode() + "\"," + + "\"message\": \"" + status.getMessage() + "\"," + + "\"payload\": null" + + "}" + ); + } +} diff --git a/src/main/java/side/onetime/auth/service/CustomAdminDetailsService.java b/src/main/java/side/onetime/auth/service/CustomAdminDetailsService.java new file mode 100644 index 00000000..89196323 --- /dev/null +++ b/src/main/java/side/onetime/auth/service/CustomAdminDetailsService.java @@ -0,0 +1,52 @@ +package side.onetime.auth.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import side.onetime.auth.dto.CustomAdminDetails; +import side.onetime.domain.AdminUser; +import side.onetime.exception.CustomException; +import side.onetime.exception.status.AdminErrorStatus; +import side.onetime.repository.AdminRepository; + +@Service +@RequiredArgsConstructor +public class CustomAdminDetailsService implements UserDetailsService { + + private final AdminRepository adminRepository; + + /** + * 관리자 이름으로 관리자 정보를 로드합니다. + * + * 데이터베이스에서 주어진 관리자 이름(username)을 기반으로 관리자를 조회하고, + * CustomAdminDetails 객체로 래핑하여 반환합니다. + * + * @param username 관리자 이름 + * @return 관리자 상세 정보 (CustomAdminDetails 객체) + * @throws CustomException 관리자 이름에 해당하는 관리자가 없을 경우 예외를 발생시킵니다. + */ + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + AdminUser admin = adminRepository.findByName(username) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER)); + return new CustomAdminDetails(admin); + } + + /** + * 관리자 ID로 관리자 정보를 로드합니다. + * + * 데이터베이스에서 주어진 관리자 ID를 기반으로 관리자를 조회하고, + * CustomAdminDetails 객체로 래핑하여 반환합니다. + * + * @param adminId 관리자 ID + * @return 관리자 상세 정보 (CustomAdminDetails 객체) + * @throws CustomException 관리자 ID에 해당하는 관리자가 없을 경우 예외를 발생시킵니다. + */ + public UserDetails loadAdminByAdminId(Long adminId) throws UsernameNotFoundException { + AdminUser admin = adminRepository.findById(adminId) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER)); + return new CustomAdminDetails(admin); + } +} diff --git a/src/main/java/side/onetime/controller/AdminController.java b/src/main/java/side/onetime/controller/AdminController.java index b0d7b82a..9e5e5884 100644 --- a/src/main/java/side/onetime/controller/AdminController.java +++ b/src/main/java/side/onetime/controller/AdminController.java @@ -64,14 +64,12 @@ public ResponseEntity> loginAdminUser( * 요청 헤더에 포함된 액세스 토큰을 기반으로 로그인된 관리자 정보를 조회합니다. * 유효한 토큰이 아닐 경우 예외가 발생하며, 유효한 경우 이름, 이메일 정보를 반환합니다. * - * @param authorizationHeader Authorization 헤더에 포함된 액세스 토큰 * @return 관리자 프로필 정보가 포함된 응답 객체 */ @GetMapping("/profile") - public ResponseEntity> getAdminUserProfile( - @RequestHeader("Authorization") String authorizationHeader) { + public ResponseEntity> getAdminUserProfile() { - GetAdminUserProfileResponse response = adminService.getAdminUserProfile(authorizationHeader); + GetAdminUserProfileResponse response = adminService.getAdminUserProfile(); return ApiResponse.onSuccess(SuccessStatus._GET_ADMIN_USER_PROFILE, response); } @@ -83,14 +81,12 @@ public ResponseEntity> getAdminUserProf * * 마스터 관리자가 아닐 경우 예외가 발생하며, 유효한 경우 모든 관리자 이름, 이메일, 상태 정보가 포함됩니다. * - * @param authorizationHeader Authorization 헤더에 포함된 액세스 토큰 * @return 전체 관리자 프로필 목록이 포함된 응답 객체 */ @GetMapping("/all") - public ResponseEntity>> getAllAdminUserDetail( - @RequestHeader("Authorization") String authorizationHeader) { + public ResponseEntity>> getAllAdminUserDetail() { - List response = adminService.getAllAdminUserDetail(authorizationHeader); + List response = adminService.getAllAdminUserDetail(); return ApiResponse.onSuccess(SuccessStatus._GET_ALL_ADMIN_USER_DETAIL, response); } @@ -101,16 +97,14 @@ public ResponseEntity>> getAllAdminUse * 요청된 관리자 ID와 수정할 권한 상태를 바탕으로 권한을 변경하며, * 요청한 사용자가 마스터 관리자가 아닐 경우 예외가 발생합니다. * - * @param authorizationHeader 요청자의 액세스 토큰 * @param request 수정할 관리자 ID와 변경할 권한 상태를 담은 요청 객체 * @return 성공 응답 메시지 */ @PatchMapping("/status") public ResponseEntity> updateAdminUserStatus( - @RequestHeader("Authorization") String authorizationHeader, @Valid @RequestBody UpdateAdminUserStatusRequest request) { - adminService.updateAdminUserStatus(authorizationHeader, request); + adminService.updateAdminUserStatus(request); return ApiResponse.onSuccess(SuccessStatus._UPDATE_ADMIN_USER_STATUS); } @@ -120,14 +114,12 @@ public ResponseEntity> updateAdminUserStatus( * Authorization 헤더에 포함된 액세스 토큰을 통해 인증된 관리자 계정을 삭제합니다. * - 토큰에 포함된 ID로 관리자 정보를 조회하여 삭제합니다. * - * @param authorizationHeader Authorization 헤더에 포함된 액세스 토큰 * @return 성공 응답 메시지 */ @PostMapping("/withdraw") - public ResponseEntity> withdrawAdminUser( - @RequestHeader("Authorization") String authorizationHeader) { + public ResponseEntity> withdrawAdminUser() { - adminService.withdrawAdminUser(authorizationHeader); + adminService.withdrawAdminUser(); return ApiResponse.onSuccess(SuccessStatus._WITHDRAW_ADMIN_USER); } @@ -137,7 +129,6 @@ public ResponseEntity> withdrawAdminUser( * 정렬 기준으로는 created_date, end_time, participant_count 등이 가능하며, * 응답은 최대 20개씩 페이지 단위로 제공됩니다. * - * @param authorizationHeader Authorization 헤더 (Bearer 토큰) * @param page 조회할 페이지 번호 (1부터 시작) * @param keyword 정렬 기준 필드명 (예: "created_date", "end_time", "participant_count") * @param sorting 정렬 방향 ("asc" 또는 "desc") @@ -145,13 +136,12 @@ public ResponseEntity> withdrawAdminUser( */ @GetMapping("/dashboard/events") public ResponseEntity> getAllDashboardEvents( - @RequestHeader("Authorization") String authorizationHeader, @RequestParam(value = "page", defaultValue = "1") @Min(1) int page, @RequestParam(value = "keyword", defaultValue = "created_date") String keyword, @RequestParam(value = "sorting", defaultValue = "desc") String sorting ) { Pageable pageable = PageRequest.of(page - 1, 20); - GetAllDashboardEventsResponse response = adminService.getAllDashboardEvents(authorizationHeader, pageable, keyword, sorting); + GetAllDashboardEventsResponse response = adminService.getAllDashboardEvents(pageable, keyword, sorting); return ApiResponse.onSuccess(SuccessStatus._GET_ALL_DASHBOARD_EVENTS, response); } @@ -161,7 +151,6 @@ public ResponseEntity> getAllDashboar * 정렬 기준으로는 name, email, created_date, participation_count 등이 가능하며, * 응답은 최대 20개씩 페이지 단위로 제공됩니다. * - * @param authorizationHeader Authorization 헤더 (Bearer 토큰) * @param page 조회할 페이지 번호 (1부터 시작) * @param keyword 정렬 기준 필드명 (예: "name", "email", "created_date", "participation_count") * @param sorting 정렬 방향 ("asc" 또는 "desc") @@ -169,13 +158,12 @@ public ResponseEntity> getAllDashboar */ @GetMapping("/dashboard/users") public ResponseEntity> getAllDashboardUsers( - @RequestHeader("Authorization") String authorizationHeader, @RequestParam(value = "page", defaultValue = "1") @Min(1) int page, @RequestParam(value = "keyword", defaultValue = "created_date") String keyword, @RequestParam(value = "sorting", defaultValue = "desc") String sorting ) { Pageable pageable = PageRequest.of(page - 1, 20); - GetAllDashboardUsersResponse response = adminService.getAllDashboardUsers(authorizationHeader, pageable, keyword, sorting); + GetAllDashboardUsersResponse response = adminService.getAllDashboardUsers(pageable, keyword, sorting); return ApiResponse.onSuccess(SuccessStatus._GET_ALL_DASHBOARD_USERS, response); } } diff --git a/src/main/java/side/onetime/controller/BannerController.java b/src/main/java/side/onetime/controller/BannerController.java index 072af920..656100d7 100644 --- a/src/main/java/side/onetime/controller/BannerController.java +++ b/src/main/java/side/onetime/controller/BannerController.java @@ -31,17 +31,15 @@ public class BannerController { * 요청으로 전달된 정보를 바탕으로 새로운 배너를 등록합니다. * 기본적으로 비활성화 상태이며 삭제되지 않은 상태로 생성됩니다. * - * @param authorizationHeader 액세스 토큰 * @param request 배너 등록 요청 정보 * @param imageFile 배너 등록 이미지 객체 * @return 성공 응답 메시지 */ @PostMapping(value = "/banners/register", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity> registerBanner( - @RequestHeader("Authorization") String authorizationHeader, @Valid @RequestPart(value = "request") RegisterBannerRequest request, @RequestPart(value = "image_file") MultipartFile imageFile) { - bannerService.registerBanner(authorizationHeader, request, imageFile); + bannerService.registerBanner(request, imageFile); return ApiResponse.onSuccess(SuccessStatus._REGISTER_BANNER); } @@ -51,15 +49,14 @@ public ResponseEntity> registerBanner( * 요청으로 전달된 정보를 바탕으로 새로운 띠배너를 등록합니다. * 기본적으로 비활성화 상태이며 삭제되지 않은 상태로 생성됩니다. * - * @param authorizationHeader 액세스 토큰 * @param request 띠배너 등록 요청 정보 * @return 성공 응답 메시지 */ @PostMapping("/bar-banners/register") public ResponseEntity> registerBarBanner( - @RequestHeader("Authorization") String authorizationHeader, - @Valid @RequestBody RegisterBarBannerRequest request) { - bannerService.registerBarBanner(authorizationHeader, request); + @Valid @RequestBody RegisterBarBannerRequest request + ) { + bannerService.registerBarBanner(request); return ApiResponse.onSuccess(SuccessStatus._REGISTER_BAR_BANNER); } @@ -68,15 +65,12 @@ public ResponseEntity> registerBarBanner( * * 삭제되지 않은 배너 중, ID에 해당하는 배너를 조회합니다. * - * @param authorizationHeader 액세스 토큰 * @param id 조회할 배너 ID * @return 배너 응답 객체 */ @GetMapping("/banners/{id}") - public ResponseEntity> getBanner( - @RequestHeader("Authorization") String authorizationHeader, - @PathVariable Long id) { - GetBannerResponse response = bannerService.getBanner(authorizationHeader, id); + public ResponseEntity> getBanner(@PathVariable Long id) { + GetBannerResponse response = bannerService.getBanner(id); return ApiResponse.onSuccess(SuccessStatus._GET_BANNER, response); } @@ -85,15 +79,12 @@ public ResponseEntity> getBanner( * * 삭제되지 않은 띠배너 중, ID에 해당하는 띠배너를 조회합니다. * - * @param authorizationHeader 액세스 토큰 * @param id 조회할 띠배너 ID * @return 띠배너 응답 객체 */ @GetMapping("/bar-banners/{id}") - public ResponseEntity> getBarBanner( - @RequestHeader("Authorization") String authorizationHeader, - @PathVariable Long id) { - GetBarBannerResponse response = bannerService.getBarBanner(authorizationHeader, id); + public ResponseEntity> getBarBanner(@PathVariable Long id) { + GetBarBannerResponse response = bannerService.getBarBanner(id); return ApiResponse.onSuccess(SuccessStatus._GET_BAR_BANNER, response); } @@ -102,16 +93,14 @@ public ResponseEntity> getBarBanner( * * 삭제되지 않은 모든 배너를 조회합니다. * - * @param authorizationHeader 액세스 토큰 * @return 배너 응답 객체 리스트 */ @GetMapping("/banners/all") public ResponseEntity> getAllBanners( - @RequestHeader("Authorization") String authorizationHeader, @RequestParam(value = "page", defaultValue = "1") @Min(1) int page ) { Pageable pageable = PageRequest.of(page - 1, 20); - GetAllBannersResponse response = bannerService.getAllBanners(authorizationHeader, pageable); + GetAllBannersResponse response = bannerService.getAllBanners(pageable); return ApiResponse.onSuccess(SuccessStatus._GET_ALL_BANNERS, response); } @@ -120,16 +109,14 @@ public ResponseEntity> getAllBanners( * * 삭제되지 않은 모든 띠배너를 조회합니다. * - * @param authorizationHeader 액세스 토큰 * @return 띠배너 응답 객체 리스트 */ @GetMapping("/bar-banners/all") public ResponseEntity> getAllBarBanners( - @RequestHeader("Authorization") String authorizationHeader, @RequestParam(value = "page", defaultValue = "1") @Min(1) int page ) { Pageable pageable = PageRequest.of(page - 1, 20); - GetAllBarBannersResponse response = bannerService.getAllBarBanners(authorizationHeader, pageable); + GetAllBarBannersResponse response = bannerService.getAllBarBanners(pageable); return ApiResponse.onSuccess(SuccessStatus._GET_ALL_BAR_BANNERS, response); } @@ -162,7 +149,6 @@ public ResponseEntity> getAllActi * 전달받은 요청에서 null이 아닌 필드만 수정되며, * 삭제된 배너는 수정할 수 없습니다. * - * @param authorizationHeader 액세스 토큰 * @param id 수정할 배너 ID * @param request 수정 요청 DTO * @param imageFile 배너 수정 이미지 객체 @@ -170,12 +156,11 @@ public ResponseEntity> getAllActi */ @PatchMapping(value = "/banners/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity> updateBanner( - @RequestHeader("Authorization") String authorizationHeader, @PathVariable Long id, @Valid @RequestPart(value = "request") UpdateBannerRequest request, @RequestPart(value = "image_file", required = false) MultipartFile imageFile ) { - bannerService.updateBanner(authorizationHeader, id, request, imageFile); + bannerService.updateBanner(id, request, imageFile); return ApiResponse.onSuccess(SuccessStatus._UPDATE_BANNER); } @@ -186,18 +171,16 @@ public ResponseEntity> updateBanner( * 전달받은 요청에서 null이 아닌 필드만 수정되며, * 삭제된 띠배너는 수정할 수 없습니다. * - * @param authorizationHeader 액세스 토큰 * @param id 수정할 띠배너 ID * @param request 수정 요청 DTO * @return 성공 응답 메시지 */ @PatchMapping("/bar-banners/{id}") public ResponseEntity> updateBarBanner( - @RequestHeader("Authorization") String authorizationHeader, @PathVariable Long id, @Valid @RequestBody UpdateBarBannerRequest request ) { - bannerService.updateBarBanner(authorizationHeader, id, request); + bannerService.updateBarBanner(id, request); return ApiResponse.onSuccess(SuccessStatus._UPDATE_BAR_BANNER); } @@ -207,16 +190,12 @@ public ResponseEntity> updateBarBanner( * 배너를 DB에서 실제로 삭제하지 않고, isDeleted 플래그만 true로 변경합니다. * 해당 배너는 이후 조회되지 않으며 비활성화 상태로 간주됩니다. * - * @param authorizationHeader 액세스 토큰 * @param id 삭제할 배너 ID * @return 성공 응답 메시지 */ @DeleteMapping("/banners/{id}") - public ResponseEntity> deleteBanner( - @RequestHeader("Authorization") String authorizationHeader, - @PathVariable Long id - ) { - bannerService.deleteBanner(authorizationHeader, id); + public ResponseEntity> deleteBanner(@PathVariable Long id) { + bannerService.deleteBanner(id); return ApiResponse.onSuccess(SuccessStatus._DELETE_BANNER); } @@ -226,16 +205,12 @@ public ResponseEntity> deleteBanner( * 띠배너를 DB에서 실제로 삭제하지 않고, isDeleted 플래그만 true로 변경합니다. * 해당 띠배너는 이후 조회되지 않으며 비활성화 상태로 간주됩니다. * - * @param authorizationHeader 액세스 토큰 * @param id 삭제할 띠배너 ID * @return 성공 응답 메시지 */ @DeleteMapping("/bar-banners/{id}") - public ResponseEntity> deleteBarBanner( - @RequestHeader("Authorization") String authorizationHeader, - @PathVariable Long id - ) { - bannerService.deleteBarBanner(authorizationHeader, id); + public ResponseEntity> deleteBarBanner(@PathVariable Long id) { + bannerService.deleteBarBanner(id); return ApiResponse.onSuccess(SuccessStatus._DELETE_BAR_BANNER); } diff --git a/src/main/java/side/onetime/global/common/status/ErrorStatus.java b/src/main/java/side/onetime/global/common/status/ErrorStatus.java index 0f8419b9..f8dccdd7 100644 --- a/src/main/java/side/onetime/global/common/status/ErrorStatus.java +++ b/src/main/java/side/onetime/global/common/status/ErrorStatus.java @@ -17,7 +17,8 @@ public enum ErrorStatus implements BaseErrorCode { _METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "E_METHOD_NOT_ALLOWED", "허용되지 않은 요청 메소드입니다."), _UNSUPPORTED_MEDIA_TYPE(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "E_UNSUPPORTED_MEDIA_TYPE", "지원되지 않는 미디어 타입입니다."), _NOT_FOUND_HANDLER(HttpStatus.NOT_FOUND, "E_NOT_FOUND_HANDLER", "해당 경로에 대한 핸들러를 찾을 수 없습니다."), - _FAILED_TRANSLATE_SWAGGER(HttpStatus.INTERNAL_SERVER_ERROR, "E_FAILED_TRANSLATE_SWAGGER", "Rest Docs로 생성된 json파일을 통한 스웨거 변환에 실패하였습니다.") + _FAILED_TRANSLATE_SWAGGER(HttpStatus.INTERNAL_SERVER_ERROR, "E_FAILED_TRANSLATE_SWAGGER", "Rest Docs로 생성된 json파일을 통한 스웨거 변환에 실패하였습니다."), + _UNIDENTIFIED_USER(HttpStatus.INTERNAL_SERVER_ERROR, "E_UNIDENTIFIED_USER", "인증 정보를 처리하는 과정에서 서버 오류가 발생했습니다."), ; private final HttpStatus httpStatus; diff --git a/src/main/java/side/onetime/global/config/SecurityConfig.java b/src/main/java/side/onetime/global/config/SecurityConfig.java index c5d231d2..810e2b92 100644 --- a/src/main/java/side/onetime/global/config/SecurityConfig.java +++ b/src/main/java/side/onetime/global/config/SecurityConfig.java @@ -1,7 +1,6 @@ package side.onetime.global.config; -import java.util.Arrays; - +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -13,12 +12,14 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; - -import lombok.RequiredArgsConstructor; +import side.onetime.auth.exception.CustomAccessDeniedHandler; +import side.onetime.auth.exception.CustomAuthenticationEntryPoint; import side.onetime.auth.handler.OAuthLoginFailureHandler; import side.onetime.auth.handler.OAuthLoginSuccessHandler; import side.onetime.global.filter.JwtFilter; +import java.util.Arrays; + @RequiredArgsConstructor @Configuration @EnableWebSecurity @@ -27,31 +28,42 @@ public class SecurityConfig { private final JwtFilter jwtFilter; private final OAuthLoginSuccessHandler oAuthLoginSuccessHandler; private final OAuthLoginFailureHandler oAuthLoginFailureHandler; + private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; + private final CustomAccessDeniedHandler customAccessDeniedHandler; private static final String[] SWAGGER_URLS = { "/swagger-ui/**", "/v3/api-docs/**" }; private static final String[] PUBLIC_URLS = { + "/", "/login/**", "/api/v1/events/**", "/api/v1/schedules/**", "/api/v1/members/**", "/api/v1/urls/**", "/api/v1/tokens/**", - "/api/v1/admin/**", - "/api/v1/banners/**", - "/api/v1/bar-banners/**", "/api/v1/users/onboarding", "/api/v1/users/logout", + "/api/v1/admin/register", + "/api/v1/admin/login", + "/api/v1/banners/activated/all", + "/api/v1/bar-banners/activated/all", + "/api/v1/banners/*/clicks", "/actuator/health" }; - private static final String[] AUTHENTICATED_URLS = { + private static final String[] AUTHENTICATED_USER_URLS = { "/api/v1/users/**", "/api/v1/fixed-schedules/**", }; + private static final String[] AUTHENTICATED_ADMIN_URLS = { + "/api/v1/admin/**", + "/api/v1/banners/**", + "/api/v1/bar-banners/**", + }; + private static final String[] ALLOWED_ORIGINS = { "http://localhost:5173", "http://localhost:3000", @@ -108,13 +120,18 @@ public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Excepti .authorizeHttpRequests(authorize -> authorize .requestMatchers(SWAGGER_URLS).permitAll() .requestMatchers(PUBLIC_URLS).permitAll() - .requestMatchers(AUTHENTICATED_URLS).authenticated() + .requestMatchers(AUTHENTICATED_USER_URLS).hasRole("USER") + .requestMatchers(AUTHENTICATED_ADMIN_URLS).hasRole("ADMIN") .anyRequest().authenticated() ) .oauth2Login(oauth -> oauth .successHandler(oAuthLoginSuccessHandler) .failureHandler(oAuthLoginFailureHandler) ) + .exceptionHandling(exception -> exception + .authenticationEntryPoint(customAuthenticationEntryPoint) + .accessDeniedHandler(customAccessDeniedHandler) + ) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); return httpSecurity.build(); diff --git a/src/main/java/side/onetime/global/filter/JwtFilter.java b/src/main/java/side/onetime/global/filter/JwtFilter.java index d71990ea..30f80bdb 100644 --- a/src/main/java/side/onetime/global/filter/JwtFilter.java +++ b/src/main/java/side/onetime/global/filter/JwtFilter.java @@ -6,11 +6,13 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; +import side.onetime.auth.service.CustomAdminDetailsService; import side.onetime.auth.service.CustomUserDetailsService; import side.onetime.exception.CustomException; import side.onetime.util.JwtUtil; @@ -24,6 +26,7 @@ public class JwtFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final CustomUserDetailsService customUserDetailsService; + private final CustomAdminDetailsService customAdminDetailsService; /** * 요청을 처리하며 JWT 검증 및 인증 설정을 수행합니다. @@ -43,12 +46,23 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse return; } + String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + try { - String authorizationHeader = request.getHeader("Authorization"); String token = jwtUtil.getTokenFromHeader(authorizationHeader); jwtUtil.validateToken(token); + + String userType = jwtUtil.getClaimFromToken(token, "userType", String.class); Long userId = jwtUtil.getClaimFromToken(token, "userId", Long.class); - setAuthentication(userId); + + UserDetails userDetails = "ADMIN".equals(userType) + ? customAdminDetailsService.loadAdminByAdminId(userId) + : customUserDetailsService.loadUserByUserId(userId); + setAuthentication(userDetails); filterChain.doFilter(request, response); @@ -61,10 +75,9 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse /** * 인증 정보를 SecurityContext에 설정합니다. * - * @param userId 인증된 사용자의 ID + * @param userDetails 인증된 사용자 */ - private void setAuthentication(Long userId) { - UserDetails userDetails = customUserDetailsService.loadUserByUserId(userId); + private void setAuthentication(UserDetails userDetails) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, @@ -82,47 +95,12 @@ private void setAuthentication(Long userId) { @Override protected boolean shouldNotFilter(HttpServletRequest request) { String path = request.getServletPath(); - String method = request.getMethod(); - - // 공통 prefix - boolean isGet = method.equals("GET"); - boolean isPost = method.equals("POST"); - boolean isPatch = method.equals("PATCH"); return path.equals("/actuator/health") || path.equals("/") || - path.equals("/login") || - // 스웨거 path.startsWith("/swagger-ui") || path.startsWith("/v3/api-docs") || - // 로그인 없이 접근 가능한 공통 API - path.startsWith("/api/v1/admin") || - path.startsWith("/api/v1/members") || - path.startsWith("/api/v1/tokens") || - path.startsWith("/api/v1/urls") || - path.startsWith("/api/v1/banners") || - path.startsWith("/api/v1/bar-banners") || - // 유저 관련 - (isPost && path.equals("/api/v1/users/onboarding")) || - (isPost && path.equals("/api/v1/users/logout")) || - // 이벤트 관련 - (isPost && path.equals("/api/v1/events")) || - (isGet && path.matches("/api/v1/events/[^/]+$")) || - (isGet && path.matches("/api/v1/events/[^/]+/participants")) || - (isGet && path.matches("/api/v1/events/[^/]+/most")) || - (isPost && path.matches("/api/v1/events/[^/]+/most/filtering")) || - (isPatch && path.matches("/api/v1/events/[^/]+")) || - (isGet && path.matches("/api/v1/events/qr/[^/]+")) || - // 요일 스케줄 등록/조회 (비로그인) - (isPost && path.equals("/api/v1/schedules/day")) || - (isGet && path.matches("/api/v1/schedules/day/[^/]+$")) || - (isGet && path.matches("/api/v1/schedules/day/[^/]+/[^/]+$") && !path.endsWith("/user")) || - (isPost && path.matches("/api/v1/schedules/day/[^/]+/filtering")) || - // 날짜 스케줄 등록/조회 (비로그인) - (isPost && path.equals("/api/v1/schedules/date")) || - (isGet && path.matches("/api/v1/schedules/date/[^/]+$")) || - (isGet && path.matches("/api/v1/schedules/date/[^/]+/[^/]+$") && !path.endsWith("/user")) || - (isPost && path.matches("/api/v1/schedules/date/[^/]+/filtering")); + path.startsWith("/favicon.ico"); } /** diff --git a/src/main/java/side/onetime/repository/AdminRepository.java b/src/main/java/side/onetime/repository/AdminRepository.java index d48ea318..5be9c276 100644 --- a/src/main/java/side/onetime/repository/AdminRepository.java +++ b/src/main/java/side/onetime/repository/AdminRepository.java @@ -10,4 +10,6 @@ public interface AdminRepository extends JpaRepository { boolean existsAdminUsersByEmail(String email); Optional findAdminUserByEmail(String email); + + Optional findByName(String name); } diff --git a/src/main/java/side/onetime/service/AdminService.java b/src/main/java/side/onetime/service/AdminService.java index ac472700..7f5fe7de 100644 --- a/src/main/java/side/onetime/service/AdminService.java +++ b/src/main/java/side/onetime/service/AdminService.java @@ -15,6 +15,7 @@ import side.onetime.exception.CustomException; import side.onetime.exception.status.AdminErrorStatus; import side.onetime.repository.*; +import side.onetime.util.AdminAuthorizationUtil; import side.onetime.util.JwtUtil; import java.util.Comparator; @@ -84,13 +85,13 @@ public LoginAdminUserResponse loginAdminUser(LoginAdminUserRequest request) { * 관리자 정보를 조회합니다. * - 토큰이 유효하지 않거나 관리자 정보가 존재하지 않을 경우 예외가 발생합니다. * - * @param authorizationHeader Authorization 헤더에 포함된 액세스 토큰 * @return 관리자 프로필 응답 객체 */ @Transactional(readOnly = true) - public GetAdminUserProfileResponse getAdminUserProfile(String authorizationHeader) { + public GetAdminUserProfileResponse getAdminUserProfile() { - AdminUser adminUser = jwtUtil.getAdminUserFromHeader(authorizationHeader); + AdminUser adminUser = adminRepository.findById(AdminAuthorizationUtil.getLoginAdminId()) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER)); return GetAdminUserProfileResponse.from(adminUser); } @@ -103,13 +104,13 @@ public GetAdminUserProfileResponse getAdminUserProfile(String authorizationHeade * - 마스터 관리자가 아닐 경우 예외가 발생합니다. * - 토큰이 유효하지 않거나 관리자 정보가 존재하지 않을 경우 예외가 발생합니다. * - * @param authorizationHeader Authorization 헤더에 포함된 액세스 토큰 * @return 전체 관리자 정보 리스트 */ @Transactional(readOnly = true) - public List getAllAdminUserDetail(String authorizationHeader) { + public List getAllAdminUserDetail() { - AdminUser adminUser = jwtUtil.getAdminUserFromHeader(authorizationHeader); + AdminUser adminUser = adminRepository.findById(AdminAuthorizationUtil.getLoginAdminId()) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER)); if (!AdminStatus.MASTER.equals(adminUser.getAdminStatus())) { throw new CustomException(AdminErrorStatus._ONLY_CAN_MASTER_ADMIN_USER); } @@ -128,13 +129,13 @@ public List getAllAdminUserDetail(String authorizationH * - 마스터 관리자가 아닐 경우 예외가 발생합니다. * - 대상 관리자가 존재하지 않을 경우 예외가 발생합니다. * - * @param authorizationHeader 요청자의 액세스 토큰 * @param request 수정할 관리자 ID와 변경할 권한 상태를 담은 요청 객체 */ @Transactional - public void updateAdminUserStatus(String authorizationHeader, UpdateAdminUserStatusRequest request) { + public void updateAdminUserStatus(UpdateAdminUserStatusRequest request) { - AdminUser adminUser = jwtUtil.getAdminUserFromHeader(authorizationHeader); + AdminUser adminUser = adminRepository.findById(AdminAuthorizationUtil.getLoginAdminId()) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER)); if (!AdminStatus.MASTER.equals(adminUser.getAdminStatus())) { throw new CustomException(AdminErrorStatus._ONLY_CAN_MASTER_ADMIN_USER); } @@ -149,12 +150,12 @@ public void updateAdminUserStatus(String authorizationHeader, UpdateAdminUserSta * * 액세스 토큰을 기반으로 관리자 정보를 조회한 뒤 DB에서 삭제합니다. * - * @param authorizationHeader Authorization 헤더에 포함된 액세스 토큰 */ @Transactional - public void withdrawAdminUser(String authorizationHeader) { + public void withdrawAdminUser() { - AdminUser adminUser = jwtUtil.getAdminUserFromHeader(authorizationHeader); + AdminUser adminUser = adminRepository.findById(AdminAuthorizationUtil.getLoginAdminId()) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER)); adminRepository.delete(adminUser); } @@ -167,15 +168,15 @@ public void withdrawAdminUser(String authorizationHeader) { * 정렬 기준이 participant_count인 경우 메모리 내 정렬 후 페이징 처리됩니다. * 그 외 기준은 DB 정렬 및 페이징 후 결과가 반환됩니다. * - * @param authorizationHeader Authorization 헤더에서 추출한 토큰 * @param pageable 페이지 정보 (페이지 번호, 크기 등 - 정렬은 직접 처리) * @param keyword 정렬 기준 필드명 (snake_case) * @param sorting 정렬 방향 ("asc", "desc") * @return DashboardEvent 리스트 및 페이지 정보 포함 응답 DTO */ @Transactional(readOnly = true) - public GetAllDashboardEventsResponse getAllDashboardEvents(String authorizationHeader, Pageable pageable, String keyword, String sorting) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); + public GetAllDashboardEventsResponse getAllDashboardEvents(Pageable pageable, String keyword, String sorting) { + adminRepository.findById(AdminAuthorizationUtil.getLoginAdminId()) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER)); boolean isSortByParticipant = keyword.equals("participant_count"); @@ -234,15 +235,15 @@ public GetAllDashboardEventsResponse getAllDashboardEvents(String authorizationH * * 전체 유저 수를 기준으로 totalElements와 totalPages를 계산하여 PageInfo에 포함합니다. * - * @param authorizationHeader Authorization 헤더에서 추출한 토큰 * @param pageable 페이지 정보 (페이지 번호, 크기 등) * @param keyword 정렬 기준 필드 (예: name, email, created_date 등) * @param sorting 정렬 방향 ("asc" 또는 "desc") * @return DashboardUser 리스트 및 페이지 정보 포함 응답 DTO */ @Transactional(readOnly = true) - public GetAllDashboardUsersResponse getAllDashboardUsers(String authorizationHeader, Pageable pageable, String keyword, String sorting) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); + public GetAllDashboardUsersResponse getAllDashboardUsers(Pageable pageable, String keyword, String sorting) { + adminRepository.findById(AdminAuthorizationUtil.getLoginAdminId()) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER)); List users = userRepository.findAllWithSort(pageable, keyword, sorting); diff --git a/src/main/java/side/onetime/service/BannerService.java b/src/main/java/side/onetime/service/BannerService.java index 194b212b..f9997007 100644 --- a/src/main/java/side/onetime/service/BannerService.java +++ b/src/main/java/side/onetime/service/BannerService.java @@ -16,9 +16,10 @@ import side.onetime.dto.banner.response.*; import side.onetime.exception.CustomException; import side.onetime.exception.status.AdminErrorStatus; +import side.onetime.repository.AdminRepository; import side.onetime.repository.BannerRepository; import side.onetime.repository.BarBannerRepository; -import side.onetime.util.JwtUtil; +import side.onetime.util.AdminAuthorizationUtil; import side.onetime.util.S3Util; import java.util.List; @@ -30,7 +31,7 @@ public class BannerService { private final BannerRepository bannerRepository; private final BarBannerRepository barBannerRepository; - private final JwtUtil jwtUtil; + private final AdminRepository adminRepository; private final S3Util s3Util; /** @@ -39,13 +40,13 @@ public class BannerService { * 요청 정보를 바탕으로 배너를 등록합니다. * 기본적으로 비활성화 및 삭제되지 않은 상태로 저장됩니다. * - * @param authorizationHeader 요청자의 액세스 토큰 * @param request 배너 등록 요청 객체 * @param imageFile 배너 등록 이미지 객체 */ @Transactional - public void registerBanner(String authorizationHeader, RegisterBannerRequest request, MultipartFile imageFile) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); + public void registerBanner(RegisterBannerRequest request, MultipartFile imageFile) { + adminRepository.findById(AdminAuthorizationUtil.getLoginAdminId()) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER)); Banner newBanner = bannerRepository.save(request.toEntity()); String imageUrl = uploadBannerImage(newBanner.getId(), imageFile); @@ -58,12 +59,12 @@ public void registerBanner(String authorizationHeader, RegisterBannerRequest req * 요청 정보를 바탕으로 배너를 등록합니다. * 기본적으로 비활성화 및 삭제되지 않은 상태로 저장됩니다. * - * @param authorizationHeader 요청자의 액세스 토큰 * @param request 띠배너 등록 요청 객체 */ @Transactional - public void registerBarBanner(String authorizationHeader, RegisterBarBannerRequest request) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); + public void registerBarBanner(RegisterBarBannerRequest request) { + adminRepository.findById(AdminAuthorizationUtil.getLoginAdminId()) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER)); BarBanner newBarBanner = request.toEntity(); barBannerRepository.save(newBarBanner); } @@ -74,13 +75,13 @@ public void registerBarBanner(String authorizationHeader, RegisterBarBannerReque * 삭제되지 않은 상태의 배너를 ID 기준으로 조회합니다. * 해당 배너가 존재하지 않을 경우 예외가 발생합니다. * - * @param authorizationHeader 요청자의 액세스 토큰 * @param id 조회할 배너 ID * @return 배너 응답 객체 */ @Transactional(readOnly = true) - public GetBannerResponse getBanner(String authorizationHeader, Long id) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); + public GetBannerResponse getBanner(Long id) { + adminRepository.findById(AdminAuthorizationUtil.getLoginAdminId()) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER)); Banner banner = bannerRepository.findByIdAndIsDeletedFalse(id) .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_BANNER)); return GetBannerResponse.from(banner); @@ -92,13 +93,13 @@ public GetBannerResponse getBanner(String authorizationHeader, Long id) { * 삭제되지 않은 상태의 배너를 ID 기준으로 조회합니다. * 해당 띠배너가 존재하지 않을 경우 예외가 발생합니다. * - * @param authorizationHeader 요청자의 액세스 토큰 * @param id 조회할 띠배너 ID * @return 띠배너 응답 객체 */ @Transactional(readOnly = true) - public GetBarBannerResponse getBarBanner(String authorizationHeader, Long id) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); + public GetBarBannerResponse getBarBanner(Long id) { + adminRepository.findById(AdminAuthorizationUtil.getLoginAdminId()) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER)); BarBanner barBanner = barBannerRepository.findByIdAndIsDeletedFalse(id) .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_BAR_BANNER)); return GetBarBannerResponse.from(barBanner); @@ -109,12 +110,12 @@ public GetBarBannerResponse getBarBanner(String authorizationHeader, Long id) { * * 삭제되지 않은 모든 배너를 조회하여 응답 객체로 반환합니다. * - * @param authorizationHeader 요청자의 액세스 토큰 * @return 배너 응답 객체 리스트 */ @Transactional(readOnly = true) - public GetAllBannersResponse getAllBanners(String authorizationHeader, Pageable pageable) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); + public GetAllBannersResponse getAllBanners(Pageable pageable) { + adminRepository.findById(AdminAuthorizationUtil.getLoginAdminId()) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER)); List banners = bannerRepository.findAllByIsDeletedFalseOrderByCreatedDateDesc(pageable).stream() .map(GetBannerResponse::from) @@ -138,12 +139,12 @@ public GetAllBannersResponse getAllBanners(String authorizationHeader, Pageable * * 삭제되지 않은 모든 띠배너를 조회하여 응답 객체로 반환합니다. * - * @param authorizationHeader 요청자의 액세스 토큰 * @return 띠배너 응답 객체 리스트 */ @Transactional(readOnly = true) - public GetAllBarBannersResponse getAllBarBanners(String authorizationHeader, Pageable pageable) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); + public GetAllBarBannersResponse getAllBarBanners(Pageable pageable) { + adminRepository.findById(AdminAuthorizationUtil.getLoginAdminId()) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER)); List barBanners = barBannerRepository.findAllByIsDeletedFalseOrderByCreatedDateDesc(pageable).stream() .map(GetBarBannerResponse::from) @@ -200,14 +201,14 @@ public GetAllActivatedBarBannersResponse getAllActivatedBarBanners() { * 삭제되지 않은 배너를 ID 기준으로 조회합니다. * 요청 객체에서 null이 아닌 필드만 선택적으로 수정합니다. * - * @param authorizationHeader 요청자의 액세스 토큰 * @param id 수정할 배너 ID * @param request 수정 요청 객체 * @param imageFile 배너 수정 이미지 객체 */ @Transactional - public void updateBanner(String authorizationHeader, Long id, UpdateBannerRequest request, MultipartFile imageFile) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); + public void updateBanner(Long id, UpdateBannerRequest request, MultipartFile imageFile) { + adminRepository.findById(AdminAuthorizationUtil.getLoginAdminId()) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER)); Banner banner = bannerRepository.findByIdAndIsDeletedFalse(id) .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_BANNER)); @@ -233,13 +234,13 @@ public void updateBanner(String authorizationHeader, Long id, UpdateBannerReques * 삭제되지 않은 띠배너를 ID 기준으로 조회합니다. * 요청 객체에서 null이 아닌 필드만 선택적으로 수정합니다. * - * @param authorizationHeader 요청자의 액세스 토큰 * @param id 수정할 띠배너 ID * @param request 수정 요청 객체 */ @Transactional - public void updateBarBanner(String authorizationHeader, Long id, UpdateBarBannerRequest request) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); + public void updateBarBanner(Long id, UpdateBarBannerRequest request) { + adminRepository.findById(AdminAuthorizationUtil.getLoginAdminId()) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER)); BarBanner barBanner = barBannerRepository.findByIdAndIsDeletedFalse(id) .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_BAR_BANNER)); @@ -257,12 +258,12 @@ public void updateBarBanner(String authorizationHeader, Long id, UpdateBarBanner * 삭제되지 않은 배너를 ID 기준으로 조회합니다. * 해당 배너의 삭제 상태를 true로 변경하고 S3에 저장된 이미지를 삭제합니다. * - * @param authorizationHeader 요청자의 액세스 토큰 * @param id 삭제할 배너 ID */ @Transactional - public void deleteBanner(String authorizationHeader, Long id) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); + public void deleteBanner(Long id) { + adminRepository.findById(AdminAuthorizationUtil.getLoginAdminId()) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER)); Banner banner = bannerRepository.findByIdAndIsDeletedFalse(id) .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_BANNER)); banner.markAsDeleted(); @@ -277,12 +278,12 @@ public void deleteBanner(String authorizationHeader, Long id) { * 삭제되지 않은 띠배너를 ID 기준으로 조회합니다. * 해당 배너의 삭제 상태를 true로 변경합니다. * - * @param authorizationHeader 요청자의 액세스 토큰 * @param id 삭제할 띠배너 ID */ @Transactional - public void deleteBarBanner(String authorizationHeader, Long id) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); + public void deleteBarBanner(Long id) { + adminRepository.findById(AdminAuthorizationUtil.getLoginAdminId()) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER)); BarBanner barBanner = barBannerRepository.findByIdAndIsDeletedFalse(id) .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_BAR_BANNER)); barBanner.markAsDeleted(); diff --git a/src/main/java/side/onetime/util/AdminAuthorizationUtil.java b/src/main/java/side/onetime/util/AdminAuthorizationUtil.java new file mode 100644 index 00000000..208c2f3c --- /dev/null +++ b/src/main/java/side/onetime/util/AdminAuthorizationUtil.java @@ -0,0 +1,32 @@ +package side.onetime.util; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import side.onetime.auth.dto.CustomAdminDetails; +import side.onetime.exception.CustomException; +import side.onetime.global.common.status.ErrorStatus; + +import java.util.Optional; + +public class AdminAuthorizationUtil { + + private AdminAuthorizationUtil() { + throw new AssertionError(); + } + + /** + * 현재 로그인한 관리자의 ID를 반환하는 메서드. + * + * SecurityContextHolder에서 Authentication을 가져와 + * CustomAdminDetails로 캐스팅한 후, 관리자 ID를 추출합니다. + * + * @return 로그인된 관리자의 ID + */ + public static Long getLoginAdminId() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getPrincipal) + .filter(principal -> principal instanceof CustomAdminDetails) + .map(principal -> ((CustomAdminDetails) principal).getId()) + .orElseThrow(() -> new CustomException(ErrorStatus._UNIDENTIFIED_USER)); + } +} diff --git a/src/main/java/side/onetime/util/JwtUtil.java b/src/main/java/side/onetime/util/JwtUtil.java index 4438f0d3..a1be4d76 100644 --- a/src/main/java/side/onetime/util/JwtUtil.java +++ b/src/main/java/side/onetime/util/JwtUtil.java @@ -8,13 +8,10 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import side.onetime.domain.AdminUser; import side.onetime.domain.User; import side.onetime.exception.CustomException; -import side.onetime.exception.status.AdminErrorStatus; import side.onetime.exception.status.TokenErrorStatus; import side.onetime.exception.status.UserErrorStatus; -import side.onetime.repository.AdminRepository; import side.onetime.repository.UserRepository; import javax.crypto.SecretKey; @@ -47,7 +44,6 @@ public class JwtUtil { private String browserIdSalt; private final UserRepository userRepository; - private final AdminRepository adminRepository; /** * JWT 서명 키를 생성 및 반환. @@ -180,20 +176,6 @@ public User getUserFromHeader(String authorizationHeader) { .orElseThrow(() -> new CustomException(UserErrorStatus._NOT_FOUND_USER)); } - /** - * 헤더에서 어드민 유저 객체 반환. - * - * @param authorizationHeader Authorization 헤더 - * @return 어드민 유저 객체 - */ - public AdminUser getAdminUserFromHeader(String authorizationHeader) { - String token = getTokenFromHeader(authorizationHeader); - validateToken(token); - - return adminRepository.findById(getClaimFromToken(token, "userId", Long.class)) - .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER)); - } - /** * JWT 토큰 검증. * diff --git a/src/main/java/side/onetime/util/UserAuthorizationUtil.java b/src/main/java/side/onetime/util/UserAuthorizationUtil.java index 29632d9b..96791399 100644 --- a/src/main/java/side/onetime/util/UserAuthorizationUtil.java +++ b/src/main/java/side/onetime/util/UserAuthorizationUtil.java @@ -4,7 +4,9 @@ import org.springframework.security.core.context.SecurityContextHolder; import side.onetime.auth.dto.CustomUserDetails; import side.onetime.exception.CustomException; -import side.onetime.exception.status.UserErrorStatus; +import side.onetime.global.common.status.ErrorStatus; + +import java.util.Optional; public class UserAuthorizationUtil { @@ -21,12 +23,10 @@ private UserAuthorizationUtil() { * @return 로그인된 사용자의 ID */ public static Long getLoginUserId() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - Object principal = authentication.getPrincipal(); - - if (!(principal instanceof CustomUserDetails userDetails)) { - throw new CustomException(UserErrorStatus._UNAUTHORIZED); - } - return userDetails.getId(); + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getPrincipal) + .filter(principal -> principal instanceof CustomUserDetails) + .map(principal -> ((CustomUserDetails) principal).getId()) + .orElseThrow(() -> new CustomException(ErrorStatus._UNIDENTIFIED_USER)); } } diff --git a/src/test/java/side/onetime/admin/AdminControllerTest.java b/src/test/java/side/onetime/admin/AdminControllerTest.java index d94dce7c..15fe522f 100644 --- a/src/test/java/side/onetime/admin/AdminControllerTest.java +++ b/src/test/java/side/onetime/admin/AdminControllerTest.java @@ -12,8 +12,7 @@ import org.springframework.http.MediaType; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; -import side.onetime.auth.service.CustomUserDetailsService; -import side.onetime.configuration.ControllerTestConfig; +import side.onetime.configuration.AdminControllerTestConfig; import side.onetime.controller.AdminController; import side.onetime.domain.enums.AdminStatus; import side.onetime.domain.enums.Category; @@ -23,7 +22,6 @@ import side.onetime.dto.admin.request.UpdateAdminUserStatusRequest; import side.onetime.dto.admin.response.*; import side.onetime.service.AdminService; -import side.onetime.util.JwtUtil; import java.util.List; @@ -36,17 +34,11 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(AdminController.class) -public class AdminControllerTest extends ControllerTestConfig { +public class AdminControllerTest extends AdminControllerTestConfig { @MockBean private AdminService adminService; - @MockBean - private JwtUtil jwtUtil; - - @MockBean - private CustomUserDetailsService customUserDetailsService; - @Test @DisplayName("관리자 계정 회원가입을 진행한다.") public void registerAdminUser() throws Exception { @@ -146,20 +138,17 @@ public void loginAdminUser() throws Exception { @DisplayName("관리자 프로필 조회를 진행한다.") public void getAdminUserProfile() throws Exception { // given - String accessToken = "Bearer temp.jwt.access.token"; - GetAdminUserProfileResponse response = new GetAdminUserProfileResponse( "관리자 이름", "admin@example.com" ); // when - Mockito.when(adminService.getAdminUserProfile(any(String.class))) + Mockito.when(adminService.getAdminUserProfile()) .thenReturn(response); // then - mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/admin/profile") - .header("Authorization", accessToken)) + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/admin/profile")) .andExpect(status().isOk()) .andExpect(jsonPath("$.is_success").value(true)) .andExpect(jsonPath("$.code").value("200")) @@ -191,20 +180,17 @@ public void getAdminUserProfile() throws Exception { @DisplayName("전체 관리자 정보를 조회한다.") public void getAllAdminUserDetail() throws Exception { // given - String accessToken = "Bearer temp.jwt.access.token"; - List response = List.of( new AdminUserDetailResponse(1L, "마스터 관리자", "master@example.com", AdminStatus.MASTER), new AdminUserDetailResponse(2L, "일반 관리자", "admin@example.com", AdminStatus.APPROVED) ); // when - Mockito.when(adminService.getAllAdminUserDetail(any(String.class))) + Mockito.when(adminService.getAllAdminUserDetail()) .thenReturn(response); // then - mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/admin/all") - .header("Authorization", accessToken)) + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/admin/all")) .andExpect(status().isOk()) .andExpect(jsonPath("$.is_success").value(true)) .andExpect(jsonPath("$.code").value("200")) @@ -240,18 +226,16 @@ public void getAllAdminUserDetail() throws Exception { @DisplayName("관리자 권한을 수정한다.") public void updateAdminUserStatus() throws Exception { // given - String accessToken = "Bearer temp.jwt.access.token"; String requestContent = objectMapper.writeValueAsString( new UpdateAdminUserStatusRequest(2L, AdminStatus.APPROVED) ); // when - Mockito.doNothing().when(adminService).updateAdminUserStatus(any(String.class), any(UpdateAdminUserStatusRequest.class)); + Mockito.doNothing().when(adminService).updateAdminUserStatus(any(UpdateAdminUserStatusRequest.class)); // then mockMvc.perform(RestDocumentationRequestBuilders.patch("/api/v1/admin/status") .contentType(MediaType.APPLICATION_JSON) - .header("Authorization", accessToken) .content(requestContent)) .andExpect(status().isOk()) .andExpect(jsonPath("$.is_success").value(true)) @@ -283,15 +267,11 @@ public void updateAdminUserStatus() throws Exception { @Test @DisplayName("관리자 계정을 탈퇴한다.") public void withdrawAdminUser() throws Exception { - // given - String accessToken = "Bearer temp.jwt.access.token"; - - // when - Mockito.doNothing().when(adminService).withdrawAdminUser(any(String.class)); + // given & when + Mockito.doNothing().when(adminService).withdrawAdminUser(); // then - mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/admin/withdraw") - .header("Authorization", accessToken)) + mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/admin/withdraw")) .andExpect(status().isOk()) .andExpect(jsonPath("$.is_success").value(true)) .andExpect(jsonPath("$.code").value("200")) @@ -318,8 +298,6 @@ public void withdrawAdminUser() throws Exception { @DisplayName("관리자 이벤트 대시보드 정보를 조회한다.") public void getAllDashboardEvents() throws Exception { // given - String accessToken = "Bearer temp.jwt.access.token"; - List events = List.of( new DashboardEvent( 100L, "1", "이벤트 제목", "10:00", "12:00", @@ -332,12 +310,11 @@ public void getAllDashboardEvents() throws Exception { GetAllDashboardEventsResponse response = GetAllDashboardEventsResponse.of(events, pageInfo); // when - Mockito.when(adminService.getAllDashboardEvents(any(String.class), any(Pageable.class), any(String.class), any(String.class))) + Mockito.when(adminService.getAllDashboardEvents(any(Pageable.class), any(String.class), any(String.class))) .thenReturn(response); // then mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/admin/dashboard/events") - .header("Authorization", accessToken) .param("page", "1") .param("keyword", "created_date") .param("sorting", "desc")) @@ -395,8 +372,6 @@ public void getAllDashboardEvents() throws Exception { @DisplayName("관리자 유저 대시보드 정보를 조회한다.") public void getAllDashboardUsers() throws Exception { // given - String accessToken = "Bearer temp.jwt.access.token"; - List users = List.of( new DashboardUser( 1L, @@ -420,12 +395,11 @@ public void getAllDashboardUsers() throws Exception { GetAllDashboardUsersResponse response = GetAllDashboardUsersResponse.of(users, pageInfo); // when - Mockito.when(adminService.getAllDashboardUsers(any(String.class), any(Pageable.class), any(String.class), any(String.class))) + Mockito.when(adminService.getAllDashboardUsers(any(Pageable.class), any(String.class), any(String.class))) .thenReturn(response); // then mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/admin/dashboard/users") - .header("Authorization", accessToken) .param("page", "1") .param("keyword", "created_date") .param("sorting", "desc")) diff --git a/src/test/java/side/onetime/banner/BannerControllerTest.java b/src/test/java/side/onetime/banner/BannerControllerTest.java index f71dfd20..14c462b8 100644 --- a/src/test/java/side/onetime/banner/BannerControllerTest.java +++ b/src/test/java/side/onetime/banner/BannerControllerTest.java @@ -14,8 +14,7 @@ import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.web.multipart.MultipartFile; -import side.onetime.auth.service.CustomUserDetailsService; -import side.onetime.configuration.ControllerTestConfig; +import side.onetime.configuration.AdminControllerTestConfig; import side.onetime.controller.BannerController; import side.onetime.dto.admin.response.PageInfo; import side.onetime.dto.banner.request.RegisterBannerRequest; @@ -24,7 +23,6 @@ import side.onetime.dto.banner.request.UpdateBarBannerRequest; import side.onetime.dto.banner.response.*; import side.onetime.service.BannerService; -import side.onetime.util.JwtUtil; import java.util.List; @@ -39,23 +37,15 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(BannerController.class) -public class BannerControllerTest extends ControllerTestConfig { +public class BannerControllerTest extends AdminControllerTestConfig { @MockBean private BannerService bannerService; - @MockBean - private JwtUtil jwtUtil; - - @MockBean - private CustomUserDetailsService customUserDetailsService; - @Test @DisplayName("배너를 등록한다.") public void registerBanner() throws Exception { // given - String accessToken = "Bearer test.jwt.token"; - RegisterBannerRequest request = new RegisterBannerRequest( "OneTime", "OneTime's Title", @@ -67,13 +57,12 @@ public void registerBanner() throws Exception { String requestContent = objectMapper.writeValueAsString(request); // when - Mockito.doNothing().when(bannerService).registerBanner(any(String.class), any(RegisterBannerRequest.class), any(MultipartFile.class)); + Mockito.doNothing().when(bannerService).registerBanner(any(RegisterBannerRequest.class), any(MultipartFile.class)); // then mockMvc.perform(RestDocumentationRequestBuilders.multipart("/api/v1/banners/register") .file(new MockMultipartFile("request", "", "application/json", requestContent.getBytes())) - .file(new MockMultipartFile("image_file", "banner.png", "image/png", "banner-image-content".getBytes())) - .header("Authorization", accessToken)) + .file(new MockMultipartFile("image_file", "banner.png", "image/png", "banner-image-content".getBytes()))) .andExpect(status().isCreated()) .andExpect(jsonPath("$.is_success").value(true)) .andExpect(jsonPath("$.code").value("201")) @@ -99,7 +88,6 @@ public void registerBanner() throws Exception { @DisplayName("띠배너를 등록한다.") public void registerBarBanner() throws Exception { // given - String accessToken = "Bearer test.jwt.token"; RegisterBarBannerRequest request = new RegisterBarBannerRequest( "최신 소식 안내", "News", @@ -110,12 +98,11 @@ public void registerBarBanner() throws Exception { String requestContent = objectMapper.writeValueAsString(request); // when - Mockito.doNothing().when(bannerService).registerBarBanner(any(String.class), any(RegisterBarBannerRequest.class)); + Mockito.doNothing().when(bannerService).registerBarBanner(any(RegisterBarBannerRequest.class)); // then mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/bar-banners/register") .contentType(MediaType.APPLICATION_JSON) - .header("Authorization", accessToken) .content(requestContent)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.is_success").value(true)) @@ -151,18 +138,16 @@ public void registerBarBanner() throws Exception { @DisplayName("배너를 단건 조회한다.") public void getBanner() throws Exception { // given - String accessToken = "Bearer test.jwt.token"; Long bannerId = 1L; GetBannerResponse response = new GetBannerResponse( bannerId, "OneTime", "OneTime's Title", "OneTime's Sub Title", "OneTime's Button Text", "#FFFFFF", "https://www.image.com", true, "2025-08-26 12:00:00", "https://www.link.com", 1L ); // when - Mockito.when(bannerService.getBanner(any(String.class), any(Long.class))).thenReturn(response); + Mockito.when(bannerService.getBanner(any(Long.class))).thenReturn(response); // then - mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/banners/{id}", bannerId) - .header("Authorization", accessToken)) + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/banners/{id}", bannerId)) .andExpect(status().isOk()) .andExpect(jsonPath("$.is_success").value(true)) .andExpect(jsonPath("$.code").value("200")) @@ -204,7 +189,6 @@ public void getBanner() throws Exception { @DisplayName("띠배너를 단건 조회한다.") public void getBarBanner() throws Exception { // given - String accessToken = "Bearer test.jwt.token"; Long barBannerId = 1L; GetBarBannerResponse response = new GetBarBannerResponse( barBannerId, @@ -212,11 +196,10 @@ public void getBarBanner() throws Exception { ); // when - Mockito.when(bannerService.getBarBanner(any(String.class), any(Long.class))).thenReturn(response); + Mockito.when(bannerService.getBarBanner(any(Long.class))).thenReturn(response); // then - mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/bar-banners/{id}", barBannerId) - .header("Authorization", accessToken)) + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/bar-banners/{id}", barBannerId)) .andExpect(status().isOk()) .andExpect(jsonPath("$.is_success").value(true)) .andExpect(jsonPath("$.code").value("200")) @@ -255,7 +238,6 @@ public void getBarBanner() throws Exception { @DisplayName("배너를 전체 조회한다.") public void getAllBanners() throws Exception { // given - String accessToken = "Bearer test.jwt.token"; int page = 1; List banners = List.of( @@ -267,12 +249,11 @@ public void getAllBanners() throws Exception { GetAllBannersResponse response = GetAllBannersResponse.of(banners, pageInfo); // when - Mockito.when(bannerService.getAllBanners(any(String.class), any(Pageable.class))) + Mockito.when(bannerService.getAllBanners(any(Pageable.class))) .thenReturn(response); // then mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/banners/all") - .header("Authorization", accessToken) .param("page", String.valueOf(page))) .andExpect(status().isOk()) .andExpect(jsonPath("$.is_success").value(true)) @@ -326,7 +307,6 @@ public void getAllBanners() throws Exception { @DisplayName("띠배너를 전체 조회한다.") public void getAllBarBanners() throws Exception { // given - String accessToken = "Bearer test.jwt.token"; int page = 1; List barBanners = List.of( @@ -338,12 +318,11 @@ public void getAllBarBanners() throws Exception { GetAllBarBannersResponse response = GetAllBarBannersResponse.of(barBanners, pageInfo); // when - Mockito.when(bannerService.getAllBarBanners(any(String.class), any(Pageable.class))) + Mockito.when(bannerService.getAllBarBanners(any(Pageable.class))) .thenReturn(response); // then mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/bar-banners/all") - .header("Authorization", accessToken) .param("page", String.valueOf(page))) .andExpect(status().isOk()) .andExpect(jsonPath("$.is_success").value(true)) @@ -492,7 +471,6 @@ public void getAllActivatedBarBanners() throws Exception { public void updateBanner() throws Exception { // given Long bannerId = 1L; - String accessToken = "Bearer temp.jwt.access.token"; UpdateBannerRequest request = new UpdateBannerRequest( "Modified OneTime", @@ -506,13 +484,12 @@ public void updateBanner() throws Exception { String requestContent = objectMapper.writeValueAsString(request); // when - Mockito.doNothing().when(bannerService).updateBanner(any(String.class), any(Long.class), any(UpdateBannerRequest.class), any(MultipartFile.class)); + Mockito.doNothing().when(bannerService).updateBanner(any(Long.class), any(UpdateBannerRequest.class), any(MultipartFile.class)); // then mockMvc.perform(RestDocumentationRequestBuilders.multipart("/api/v1/banners/{id}", bannerId) .file(new MockMultipartFile("request", "", "application/json", requestContent.getBytes())) .file(new MockMultipartFile("image_file", "banner.png", "image/png", "banner-image-content".getBytes())) - .header("Authorization", accessToken) .with(r -> { r.setMethod("PATCH"); return r; @@ -543,7 +520,6 @@ public void updateBanner() throws Exception { public void updateBarBanner() throws Exception { // given Long barBannerId = 1L; - String accessToken = "Bearer temp.jwt.access.token"; UpdateBarBannerRequest request = new UpdateBarBannerRequest( "수정된 내용", "modified content", @@ -555,11 +531,10 @@ public void updateBarBanner() throws Exception { String requestContent = objectMapper.writeValueAsString(request); // when - Mockito.doNothing().when(bannerService).updateBarBanner(any(String.class), eq(barBannerId), any(UpdateBarBannerRequest.class)); + Mockito.doNothing().when(bannerService).updateBarBanner(eq(barBannerId), any(UpdateBarBannerRequest.class)); // then mockMvc.perform(RestDocumentationRequestBuilders.patch("/api/v1/bar-banners/{id}", barBannerId) - .header("Authorization", accessToken) .contentType(MediaType.APPLICATION_JSON) .content(requestContent)) .andExpect(status().isOk()) @@ -597,15 +572,13 @@ public void updateBarBanner() throws Exception { @DisplayName("배너를 삭제한다.") public void deleteBanner() throws Exception { // given - String accessToken = "Bearer test.jwt.token"; Long bannerId = 1L; // when - Mockito.doNothing().when(bannerService).deleteBanner(any(String.class), eq(bannerId)); + Mockito.doNothing().when(bannerService).deleteBanner(eq(bannerId)); // then - mockMvc.perform(RestDocumentationRequestBuilders.delete("/api/v1/banners/{id}", bannerId) - .header("Authorization", accessToken)) + mockMvc.perform(RestDocumentationRequestBuilders.delete("/api/v1/banners/{id}", bannerId)) .andExpect(status().isOk()) .andExpect(jsonPath("$.is_success").value(true)) .andExpect(jsonPath("$.code").value("200")) @@ -632,15 +605,13 @@ public void deleteBanner() throws Exception { @DisplayName("띠배너를 삭제한다.") public void deleteBarBanner() throws Exception { // given - String accessToken = "Bearer test.jwt.token"; Long barBannerId = 1L; // when - Mockito.doNothing().when(bannerService).deleteBarBanner(any(String.class), eq(barBannerId)); + Mockito.doNothing().when(bannerService).deleteBarBanner(eq(barBannerId)); // then - mockMvc.perform(RestDocumentationRequestBuilders.delete("/api/v1/bar-banners/{id}", barBannerId) - .header("Authorization", accessToken)) + mockMvc.perform(RestDocumentationRequestBuilders.delete("/api/v1/bar-banners/{id}", barBannerId)) .andExpect(status().isOk()) .andExpect(jsonPath("$.is_success").value(true)) .andExpect(jsonPath("$.code").value("200")) diff --git a/src/test/java/side/onetime/configuration/AdminControllerTestConfig.java b/src/test/java/side/onetime/configuration/AdminControllerTestConfig.java new file mode 100644 index 00000000..c46806ac --- /dev/null +++ b/src/test/java/side/onetime/configuration/AdminControllerTestConfig.java @@ -0,0 +1,22 @@ +package side.onetime.configuration; + +import org.junit.jupiter.api.BeforeEach; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import side.onetime.auth.dto.CustomAdminDetails; +import side.onetime.domain.AdminUser; +import side.onetime.domain.enums.AdminStatus; + +public abstract class AdminControllerTestConfig extends ControllerTestConfig { + + @BeforeEach + void setAdminAuthentication() { + AdminUser mockAdmin = AdminUser.builder().name("testAdmin").email("admin@example.com").build(); + mockAdmin.updateAdminStatus(AdminStatus.MASTER); + + CustomAdminDetails customAdminDetails = new CustomAdminDetails(mockAdmin); + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(customAdminDetails, null, customAdminDetails.getAuthorities()) + ); + } +} diff --git a/src/test/java/side/onetime/configuration/ControllerTestConfig.java b/src/test/java/side/onetime/configuration/ControllerTestConfig.java index eac30e4f..08300ac4 100644 --- a/src/test/java/side/onetime/configuration/ControllerTestConfig.java +++ b/src/test/java/side/onetime/configuration/ControllerTestConfig.java @@ -6,16 +6,17 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.filter.CharacterEncodingFilter; -import side.onetime.auth.dto.CustomUserDetails; -import side.onetime.domain.User; +import side.onetime.auth.service.CustomAdminDetailsService; +import side.onetime.auth.service.CustomUserDetailsService; +import side.onetime.util.JwtUtil; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; @@ -32,6 +33,15 @@ public abstract class ControllerTestConfig { protected MockMvc mockMvc; + @MockBean + private JwtUtil jwtUtil; + + @MockBean + private CustomUserDetailsService customUserDetailsService; + + @MockBean + private CustomAdminDetailsService customAdminDetailsService; + @BeforeEach void setUp(final RestDocumentationContextProvider restDocumentation) { mockMvc = MockMvcBuilders.webAppContextSetup(context) @@ -40,10 +50,6 @@ void setUp(final RestDocumentationContextProvider restDocumentation) { .alwaysDo(print()) .build(); - User mockUser = User.builder().nickname("testUser").email("test@example.com").build(); - CustomUserDetails customUserDetails = new CustomUserDetails(mockUser); - SecurityContextHolder.getContext().setAuthentication( - new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities()) - ); + SecurityContextHolder.clearContext(); } } diff --git a/src/test/java/side/onetime/configuration/UserControllerTestConfig.java b/src/test/java/side/onetime/configuration/UserControllerTestConfig.java new file mode 100644 index 00000000..a7445171 --- /dev/null +++ b/src/test/java/side/onetime/configuration/UserControllerTestConfig.java @@ -0,0 +1,20 @@ +package side.onetime.configuration; + +import org.junit.jupiter.api.BeforeEach; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import side.onetime.auth.dto.CustomUserDetails; +import side.onetime.domain.User; + +public abstract class UserControllerTestConfig extends ControllerTestConfig { + + @BeforeEach + void setUserAuthentication() { + User mockUser = User.builder().nickname("testUser").email("test@example.com").build(); + + CustomUserDetails customUserDetails = new CustomUserDetails(mockUser); + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities()) + ); + } +} diff --git a/src/test/java/side/onetime/event/EventControllerTest.java b/src/test/java/side/onetime/event/EventControllerTest.java index d2233177..719ca018 100644 --- a/src/test/java/side/onetime/event/EventControllerTest.java +++ b/src/test/java/side/onetime/event/EventControllerTest.java @@ -14,7 +14,6 @@ import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.ResultActions; -import side.onetime.auth.service.CustomUserDetailsService; import side.onetime.configuration.ControllerTestConfig; import side.onetime.controller.EventController; import side.onetime.domain.enums.Category; @@ -24,7 +23,6 @@ import side.onetime.dto.event.response.*; import side.onetime.dto.schedule.request.GetFilteredSchedulesRequest; import side.onetime.service.EventService; -import side.onetime.util.JwtUtil; import java.time.LocalDateTime; import java.util.Collections; @@ -45,12 +43,6 @@ public class EventControllerTest extends ControllerTestConfig { @MockBean private EventService eventService; - @MockBean - private JwtUtil jwtUtil; - - @MockBean - private CustomUserDetailsService customUserDetailsService; - @Test @DisplayName("이벤트를 생성한다. (토큰 유무에 따라 로그인/비로그인 구분)") public void createEventForAnonymousUser() throws Exception { diff --git a/src/test/java/side/onetime/fixed/FixedControllerTest.java b/src/test/java/side/onetime/fixed/FixedControllerTest.java index 607ac1f8..37a144fc 100644 --- a/src/test/java/side/onetime/fixed/FixedControllerTest.java +++ b/src/test/java/side/onetime/fixed/FixedControllerTest.java @@ -1,14 +1,7 @@ package side.onetime.fixed; -import static com.epages.restdocs.apispec.ResourceDocumentation.*; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; -import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -import java.util.List; - -import org.junit.jupiter.api.BeforeEach; +import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper; +import com.epages.restdocs.apispec.ResourceSnippetParameters; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -17,48 +10,30 @@ import org.springframework.http.MediaType; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.web.servlet.ResultActions; - -import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper; -import com.epages.restdocs.apispec.ResourceSnippetParameters; - -import side.onetime.auth.dto.CustomUserDetails; -import side.onetime.auth.service.CustomUserDetailsService; -import side.onetime.configuration.ControllerTestConfig; +import side.onetime.configuration.UserControllerTestConfig; import side.onetime.controller.FixedController; -import side.onetime.domain.User; import side.onetime.dto.fixed.request.UpdateFixedScheduleRequest; import side.onetime.dto.fixed.response.FixedScheduleResponse; import side.onetime.dto.fixed.response.GetFixedScheduleResponse; import side.onetime.exception.CustomException; import side.onetime.exception.status.FixedErrorStatus; import side.onetime.service.FixedScheduleService; -import side.onetime.util.JwtUtil; -@WebMvcTest(FixedController.class) -public class FixedControllerTest extends ControllerTestConfig { +import java.util.List; - @MockBean - private FixedScheduleService fixedScheduleService; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @MockBean - private JwtUtil jwtUtil; +@WebMvcTest(FixedController.class) +public class FixedControllerTest extends UserControllerTestConfig { @MockBean - private CustomUserDetailsService customUserDetailsService; - - private CustomUserDetails customUserDetails; - - @BeforeEach - public void setupSecurityContext() { - User mockUser = User.builder().name("User").email("user@example.com").build(); - customUserDetails = new CustomUserDetails(mockUser); - SecurityContextHolder.getContext().setAuthentication( - new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities()) - ); - } + private FixedScheduleService fixedScheduleService; @Test @DisplayName("고정 스케줄을 조회한다.") diff --git a/src/test/java/side/onetime/member/MemberControllerTest.java b/src/test/java/side/onetime/member/MemberControllerTest.java index 31dc3a90..e147965b 100644 --- a/src/test/java/side/onetime/member/MemberControllerTest.java +++ b/src/test/java/side/onetime/member/MemberControllerTest.java @@ -11,7 +11,6 @@ import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.ResultActions; -import side.onetime.auth.service.CustomUserDetailsService; import side.onetime.configuration.ControllerTestConfig; import side.onetime.controller.MemberController; import side.onetime.dto.member.request.IsDuplicateRequest; @@ -22,7 +21,6 @@ import side.onetime.dto.member.response.RegisterMemberResponse; import side.onetime.dto.member.response.ScheduleResponse; import side.onetime.service.MemberService; -import side.onetime.util.JwtUtil; import java.util.List; @@ -39,12 +37,6 @@ public class MemberControllerTest extends ControllerTestConfig { @MockBean private MemberService memberService; - @MockBean - private JwtUtil jwtUtil; - - @MockBean - private CustomUserDetailsService customUserDetailsService; - @Test @DisplayName("멤버를 등록한다.") public void registerMember() throws Exception { diff --git a/src/test/java/side/onetime/schedule/ScheduleControllerTest.java b/src/test/java/side/onetime/schedule/ScheduleControllerTest.java index 7335f59d..3491ac1f 100644 --- a/src/test/java/side/onetime/schedule/ScheduleControllerTest.java +++ b/src/test/java/side/onetime/schedule/ScheduleControllerTest.java @@ -12,11 +12,8 @@ import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.ResultActions; -import side.onetime.auth.service.CustomUserDetailsService; import side.onetime.configuration.ControllerTestConfig; import side.onetime.controller.ScheduleController; -import side.onetime.domain.User; -import side.onetime.dto.event.response.GetParticipantsResponse; import side.onetime.dto.schedule.request.CreateDateScheduleRequest; import side.onetime.dto.schedule.request.CreateDayScheduleRequest; import side.onetime.dto.schedule.request.GetFilteredSchedulesRequest; @@ -25,13 +22,13 @@ import side.onetime.dto.schedule.response.PerDateSchedulesResponse; import side.onetime.dto.schedule.response.PerDaySchedulesResponse; import side.onetime.service.ScheduleService; -import side.onetime.util.JwtUtil; import java.util.List; import java.util.UUID; import static com.epages.restdocs.apispec.ResourceDocumentation.resource; -import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; @@ -40,14 +37,9 @@ @WebMvcTest(ScheduleController.class) public class ScheduleControllerTest extends ControllerTestConfig { - @MockBean - private ScheduleService scheduleService; @MockBean - private JwtUtil jwtUtil; - - @MockBean - private CustomUserDetailsService customUserDetailsService; + private ScheduleService scheduleService; @Test @DisplayName("요일 스케줄을 등록한다. (토큰 유무에 따라 로그인/비로그인 구분)") diff --git a/src/test/java/side/onetime/token/TokenControllerTest.java b/src/test/java/side/onetime/token/TokenControllerTest.java index 67ddcad9..148191da 100644 --- a/src/test/java/side/onetime/token/TokenControllerTest.java +++ b/src/test/java/side/onetime/token/TokenControllerTest.java @@ -12,13 +12,11 @@ import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.ResultActions; -import side.onetime.auth.service.CustomUserDetailsService; import side.onetime.configuration.ControllerTestConfig; import side.onetime.controller.TokenController; import side.onetime.dto.token.request.ReissueTokenRequest; import side.onetime.dto.token.response.ReissueTokenResponse; import side.onetime.service.TokenService; -import side.onetime.util.JwtUtil; import static com.epages.restdocs.apispec.ResourceDocumentation.resource; import static org.mockito.ArgumentMatchers.any; @@ -29,14 +27,9 @@ @WebMvcTest(TokenController.class) public class TokenControllerTest extends ControllerTestConfig { - @MockBean - private TokenService tokenService; @MockBean - private JwtUtil jwtUtil; - - @MockBean - private CustomUserDetailsService customUserDetailsService; + private TokenService tokenService; @Test @DisplayName("액세스 토큰을 재발행한다.") diff --git a/src/test/java/side/onetime/url/UrlControllerTest.java b/src/test/java/side/onetime/url/UrlControllerTest.java index eaf5fa0b..ce5da066 100644 --- a/src/test/java/side/onetime/url/UrlControllerTest.java +++ b/src/test/java/side/onetime/url/UrlControllerTest.java @@ -12,7 +12,6 @@ import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.ResultActions; -import side.onetime.auth.service.CustomUserDetailsService; import side.onetime.configuration.ControllerTestConfig; import side.onetime.controller.UrlController; import side.onetime.dto.url.request.ConvertToOriginalUrlRequest; @@ -21,7 +20,6 @@ import side.onetime.dto.url.response.ConvertToShortenUrlResponse; import side.onetime.repository.EventRepository; import side.onetime.service.UrlService; -import side.onetime.util.JwtUtil; import java.util.UUID; @@ -34,18 +32,13 @@ @WebMvcTest(UrlController.class) public class UrlControllerTest extends ControllerTestConfig { + @MockBean private UrlService urlService; @MockBean private EventRepository eventRepository; - @MockBean - private JwtUtil jwtUtil; - - @MockBean - private CustomUserDetailsService customUserDetailsService; - @Test @DisplayName("원본 URL을 단축 URL로 변환한다.") public void convertToShortenUrl() throws Exception { diff --git a/src/test/java/side/onetime/user/UserControllerTest.java b/src/test/java/side/onetime/user/UserControllerTest.java index a1ba22b6..6781e4de 100644 --- a/src/test/java/side/onetime/user/UserControllerTest.java +++ b/src/test/java/side/onetime/user/UserControllerTest.java @@ -12,8 +12,7 @@ import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.ResultActions; -import side.onetime.auth.service.CustomUserDetailsService; -import side.onetime.configuration.ControllerTestConfig; +import side.onetime.configuration.UserControllerTestConfig; import side.onetime.controller.UserController; import side.onetime.domain.enums.GuideType; import side.onetime.domain.enums.Language; @@ -22,7 +21,6 @@ import side.onetime.exception.CustomException; import side.onetime.exception.status.UserErrorStatus; import side.onetime.service.UserService; -import side.onetime.util.JwtUtil; import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName; import static com.epages.restdocs.apispec.ResourceDocumentation.resource; @@ -33,17 +31,11 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(UserController.class) -public class UserControllerTest extends ControllerTestConfig { +public class UserControllerTest extends UserControllerTestConfig { @MockBean private UserService userService; - @MockBean - private JwtUtil jwtUtil; - - @MockBean - private CustomUserDetailsService customUserDetailsService; - @Test @DisplayName("유저 온보딩을 진행한다.") public void onboardUser() throws Exception {