diff --git a/src/main/java/com/kamco/cd/kamcoback/auth/AuthFailureEventListener.java b/src/main/java/com/kamco/cd/kamcoback/auth/AuthFailureEventListener.java deleted file mode 100644 index 75486d9e..00000000 --- a/src/main/java/com/kamco/cd/kamcoback/auth/AuthFailureEventListener.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.kamco.cd.kamcoback.auth; - -import com.kamco.cd.kamcoback.members.service.AuthService; -import java.util.UUID; -import lombok.RequiredArgsConstructor; -import org.springframework.context.ApplicationListener; -import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class AuthFailureEventListener implements ApplicationListener { - - private final AuthService authService; - - @Override - public void onApplicationEvent(AbstractAuthenticationFailureEvent event) { - // 로그인 시도에 사용된 (username) - Object principal = event.getAuthentication().getPrincipal(); - - if (principal instanceof String username) { - // 로그인 실패 카운트 증가 로직 호출 - authService.loginFail(UUID.fromString(username)); - } - } - - @Override - public boolean supportsAsyncExecution() { - return ApplicationListener.super.supportsAsyncExecution(); - } -} diff --git a/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java b/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java index 8e6cb1bf..0de417b1 100644 --- a/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java +++ b/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java @@ -1,11 +1,13 @@ package com.kamco.cd.kamcoback.auth; +import com.kamco.cd.kamcoback.common.enums.StatusType; +import com.kamco.cd.kamcoback.common.enums.error.AuthErrorCode; +import com.kamco.cd.kamcoback.common.exception.CustomApiException; import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository; import lombok.RequiredArgsConstructor; import org.mindrot.jbcrypt.BCrypt; import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -24,18 +26,35 @@ public class CustomAuthenticationProvider implements AuthenticationProvider { String username = authentication.getName(); String rawPassword = authentication.getCredentials().toString(); - // 1. 유저 조회 + // 유저 조회 MemberEntity member = - membersRepository - .findByUserId(username) - .orElseThrow(() -> new BadCredentialsException("ID 또는 비밀번호가 일치하지 않습니다.")); + membersRepository + .findByUserId(username) + .orElseThrow(() -> new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND)); - // 2. jBCrypt + 커스텀 salt 로 저장된 패스워드 비교 + // jBCrypt + 커스텀 salt 로 저장된 패스워드 비교 if (!BCrypt.checkpw(rawPassword, member.getPassword())) { - throw new BadCredentialsException("ID 또는 비밀번호가 일치하지 않습니다."); + // 실패 카운트 저장 + int cnt = member.getLoginFailCount() + 1; + if (cnt >= 5) { + member.setStatus(StatusType.INACTIVE.getId()); + } + member.setLoginFailCount(cnt); + membersRepository.save(member); + throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH); } - // 3. 인증 성공 → UserDetails 생성 + // 삭제 상태 + if (member.getStatus().equals(StatusType.DELETED.getId())) { + throw new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND); + } + + // 패스워드 실패 횟수 체크 + if (member.getLoginFailCount() >= 5) { + throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_EXCEEDED); + } + + // 인증 성공 → UserDetails 생성 CustomUserDetails userDetails = new CustomUserDetails(member); return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); diff --git a/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java b/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java index eaa7f2d6..c3ff3942 100644 --- a/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java @@ -219,7 +219,7 @@ public class CommonCodeApiController { // .map(Clazzes::new) // .toList(); - //변화탐지 clazz API : enum -> 공통코드로 변경 + // 변화탐지 clazz API : enum -> 공통코드로 변경 List list = commonCodeUtil.getChildCodesByParentCode("0000").stream() .map( diff --git a/src/main/java/com/kamco/cd/kamcoback/common/enums/RoleType.java b/src/main/java/com/kamco/cd/kamcoback/common/enums/RoleType.java index 3da213ea..d1945c7e 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/enums/RoleType.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/RoleType.java @@ -7,7 +7,7 @@ import lombok.Getter; @Getter @AllArgsConstructor public enum RoleType implements EnumType { - ROLE_ADMIN("시스템 관리자"), + ROLE_ADMIN("관리자"), ROLE_LABELER("라벨러"), ROLE_REVIEWER("검수자"); @@ -22,4 +22,13 @@ public enum RoleType implements EnumType { public String getText() { return desc; } + + public static RoleType from(String value) { + for (RoleType type : values()) { + if (type.name().equalsIgnoreCase(value)) { + return type; + } + } + return null; + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/common/enums/StatusType.java b/src/main/java/com/kamco/cd/kamcoback/common/enums/StatusType.java new file mode 100644 index 00000000..0da73338 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/StatusType.java @@ -0,0 +1,25 @@ +package com.kamco.cd.kamcoback.common.enums; + +import com.kamco.cd.kamcoback.config.enums.EnumType; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum StatusType implements EnumType { + ACTIVE("활성"), + INACTIVE("비활성"), + DELETED("삭제"); + + private final String desc; + + @Override + public String getId() { + return name(); + } + + @Override + public String getText() { + return desc; + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java b/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java new file mode 100644 index 00000000..4f5f45dd --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java @@ -0,0 +1,22 @@ +package com.kamco.cd.kamcoback.common.enums.error; + +import com.kamco.cd.kamcoback.common.utils.ErrorCode; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum AuthErrorCode implements ErrorCode { + LOGIN_ID_NOT_FOUND("LOGIN_ID_NOT_FOUND", HttpStatus.UNAUTHORIZED), + + LOGIN_PASSWORD_MISMATCH("LOGIN_PASSWORD_MISMATCH", HttpStatus.UNAUTHORIZED), + + LOGIN_PASSWORD_EXCEEDED("LOGIN_PASSWORD_EXCEEDED", HttpStatus.UNAUTHORIZED); + + private final String code; + private final HttpStatus status; + + AuthErrorCode(String code, HttpStatus status) { + this.code = code; + this.status = status; + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/common/exception/CustomApiException.java b/src/main/java/com/kamco/cd/kamcoback/common/exception/CustomApiException.java index 482d5f6c..cd3a3a1a 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/exception/CustomApiException.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/exception/CustomApiException.java @@ -1,5 +1,6 @@ package com.kamco.cd.kamcoback.common.exception; +import com.kamco.cd.kamcoback.common.utils.ErrorCode; import lombok.Getter; import org.springframework.http.HttpStatus; @@ -19,4 +20,9 @@ public class CustomApiException extends RuntimeException { this.codeName = codeName; this.status = status; } + + public CustomApiException(ErrorCode errorCode) { + this.codeName = errorCode.getCode(); + this.status = errorCode.getStatus(); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/common/utils/ErrorCode.java b/src/main/java/com/kamco/cd/kamcoback/common/utils/ErrorCode.java new file mode 100644 index 00000000..8ace8fe1 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/utils/ErrorCode.java @@ -0,0 +1,10 @@ +package com.kamco.cd.kamcoback.common.utils; + +import org.springframework.http.HttpStatus; + +public interface ErrorCode { + + String getCode(); + + HttpStatus getStatus(); +} diff --git a/src/main/java/com/kamco/cd/kamcoback/common/utils/UserUtil.java b/src/main/java/com/kamco/cd/kamcoback/common/utils/UserUtil.java new file mode 100644 index 00000000..b404cbf8 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/utils/UserUtil.java @@ -0,0 +1,41 @@ +package com.kamco.cd.kamcoback.common.utils; + +import com.kamco.cd.kamcoback.auth.CustomUserDetails; +import com.kamco.cd.kamcoback.members.dto.MembersDto; +import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class UserUtil { + + public MembersDto.Member getCurrentUser() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .filter(auth -> auth.getPrincipal() instanceof CustomUserDetails) + .map( + auth -> { + CustomUserDetails user = (CustomUserDetails) auth.getPrincipal(); + MemberEntity m = user.getMember(); + return new MembersDto.Member(m.getId(), m.getName(), m.getEmployeeNo()); + }) + .orElse(null); + } + + public Long getId() { + MembersDto.Member user = getCurrentUser(); + return user != null ? user.getId() : null; + } + + public String getName() { + MembersDto.Member user = getCurrentUser(); + return user != null ? user.getName() : null; + } + + public String getEmployeeNo() { + MembersDto.Member user = getCurrentUser(); + return user != null ? user.getEmployeeNo() : null; + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/common/utils/html/HtmlEscapeDeserializer.java b/src/main/java/com/kamco/cd/kamcoback/common/utils/html/HtmlEscapeDeserializer.java index 7496a2d8..df1c8a32 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/utils/html/HtmlEscapeDeserializer.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/utils/html/HtmlEscapeDeserializer.java @@ -13,8 +13,6 @@ public class HtmlEscapeDeserializer extends JsonDeserializer { public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException { String value = jsonParser.getValueAsString(); - System.out.println("🔥 HtmlEscapeDeserializer 실행됨: " + value); - System.out.println("convert : " + (value == null ? null : HtmlUtils.htmlEscape(value))); return value == null ? null : HtmlUtils.htmlEscape(value); } } diff --git a/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java b/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java index a952e535..2fecb1cd 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java @@ -6,12 +6,14 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @@ -28,33 +30,33 @@ public class SecurityConfig { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.cors(cors -> cors.configurationSource(corsConfigurationSource())) - .csrf(csrf -> csrf.disable()) // CSRF 보안 기능 비활성화 - .sessionManagement( - sm -> - sm.sessionCreationPolicy( - SessionCreationPolicy.STATELESS)) // 서버 세션 만들지 않음, 요청은 JWT 인증 - .formLogin(form -> form.disable()) // react에서 로그인 요청 관리 - .httpBasic(basic -> basic.disable()) // 기본 basic 인증 비활성화 JWT 인증사용 - .logout(logout -> logout.disable()) // 기본 로그아웃 비활성화 JWT는 서버 상태가 없으므로 로그아웃 처리 필요 없음 - .authenticationProvider( - customAuthenticationProvider) // 로그인 패스워드 비교방식 스프링 기본 Provider 사용안함 커스텀 사용 - .authorizeHttpRequests( - auth -> - auth.anyRequest().permitAll()); -// requestMatchers(HttpMethod.OPTIONS, "/**") -// .permitAll() // preflight 허용 -// .requestMatchers( -// "/api/auth/signin", -// "/api/auth/refresh", -// "/swagger-ui/**", -// "/v3/api-docs/**") -// .permitAll() -// .anyRequest() -// .authenticated()) -// .addFilterBefore( -// jwtAuthenticationFilter, -// UsernamePasswordAuthenticationFilter -// .class) // 요청 들어오면 먼저 JWT 토큰 검사 후 security context 에 사용자 정보 저장. + .csrf(csrf -> csrf.disable()) // CSRF 보안 기능 비활성화 + .sessionManagement( + sm -> + sm.sessionCreationPolicy( + SessionCreationPolicy.STATELESS)) // 서버 세션 만들지 않음, 요청은 JWT 인증 + .formLogin(form -> form.disable()) // react에서 로그인 요청 관리 + .httpBasic(basic -> basic.disable()) // 기본 basic 인증 비활성화 JWT 인증사용 + .logout(logout -> logout.disable()) // 기본 로그아웃 비활성화 JWT는 서버 상태가 없으므로 로그아웃 처리 필요 없음 + .authenticationProvider( + customAuthenticationProvider) // 로그인 패스워드 비교방식 스프링 기본 Provider 사용안함 커스텀 사용 + .authorizeHttpRequests( + auth -> + auth.requestMatchers(HttpMethod.OPTIONS, "/**") + .permitAll() // preflight 허용 + .requestMatchers( + "/api/auth/signin", + "/api/auth/refresh", + "/swagger-ui/**", + "/api/members/{memberId}/password", + "/v3/api-docs/**") + .permitAll() + .anyRequest() + .authenticated()) + .addFilterBefore( + jwtAuthenticationFilter, + UsernamePasswordAuthenticationFilter + .class) // 요청 들어오면 먼저 JWT 토큰 검사 후 security context 에 사용자 정보 저장. ; return http.build(); @@ -62,7 +64,7 @@ public class SecurityConfig { @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) - throws Exception { + throws Exception { return configuration.getAuthenticationManager(); } diff --git a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java index 3cd2d792..969fde31 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java @@ -164,6 +164,9 @@ public class ApiResponseDto { NOT_FOUND_USER_FOR_EMAIL("이메일로 유저를 찾을 수 없습니다."), NOT_FOUND_USER("사용자를 찾을 수 없습니다."), UNPROCESSABLE_ENTITY("이 데이터는 삭제할 수 없습니다."), + LOGIN_ID_NOT_FOUND("아이디를 잘못 입력하셨습니다."), + LOGIN_PASSWORD_MISMATCH("비밀번호를 잘못 입력하셨습니다."), + LOGIN_PASSWORD_EXCEEDED("비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다.\n로그인 오류에 대해 관리자에게 문의하시기 바랍니다."), INVALID_EMAIL_TOKEN( "You can only reset your password within 24 hours from when the email was sent.\n" + "To reset your password again, please submit a new request through \"Forgot" diff --git a/src/main/java/com/kamco/cd/kamcoback/config/enums/EnumType.java b/src/main/java/com/kamco/cd/kamcoback/config/enums/EnumType.java index 274bfe72..1be49253 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/enums/EnumType.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/enums/EnumType.java @@ -5,4 +5,17 @@ public interface EnumType { String getId(); String getText(); + + // code로 text + static & EnumType> E fromId(Class enumClass, String id) { + if (id == null) { + return null; + } + for (E e : enumClass.getEnumConstants()) { + if (id.equalsIgnoreCase(e.getId())) { + return e; + } + } + return null; // 못 찾으면 null + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiController.java b/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiController.java index 9ee43a3b..12bd6454 100644 --- a/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiController.java @@ -28,7 +28,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @Tag(name = "추론관리 분석결과", description = "추론관리 분석결과") -@RequestMapping({"/demo/inf/res", "/api/inf/res"}) +@RequestMapping({"/api/inf/res"}) @RequiredArgsConstructor @RestController public class InferenceResultApiController { diff --git a/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java b/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java index 4ed55cda..33afc12c 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java @@ -30,78 +30,78 @@ public class AdminApiController { @Operation(summary = "관리자 계정 등록", description = "관리자 계정 등록") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "등록 성공", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Long.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "201", + description = "등록 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Long.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @PostMapping("/join") public ApiResponseDto saveMember( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "관리자 계정 등록", - required = true, - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = MembersDto.AddReq.class))) - @RequestBody - @Valid - MembersDto.AddReq addReq) { + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "관리자 계정 등록", + required = true, + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = MembersDto.AddReq.class))) + @RequestBody + @Valid + MembersDto.AddReq addReq) { return ApiResponseDto.createOK(adminService.saveMember(addReq)); } @Operation(summary = "관리자 계정 수정", description = "관리자 계정 수정") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "수정 성공", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Long.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "201", + description = "수정 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Long.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @PutMapping("/{uuid}") public ApiResponseDto updateMembers( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "관리자 계정 수정", - required = true, - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = MembersDto.UpdateReq.class))) - @PathVariable - UUID uuid, - @RequestBody @Valid MembersDto.UpdateReq updateReq) { + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "관리자 계정 수정", + required = true, + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = MembersDto.UpdateReq.class))) + @PathVariable + UUID uuid, + @RequestBody @Valid MembersDto.UpdateReq updateReq) { adminService.updateMembers(uuid, updateReq); return ApiResponseDto.createOK(UUID.randomUUID()); } @Operation(summary = "관리자 계정 미사용 처리", description = "관리자 계정 미사용 처리") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "관리자 계정 미사용 처리", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = UUID.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "201", + description = "관리자 계정 미사용 처리", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = UUID.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @DeleteMapping("/delete/{uuid}") public ApiResponseDto deleteAccount(@PathVariable UUID uuid) { adminService.deleteAccount(uuid); diff --git a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java index 47b76169..8596df04 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java @@ -1,12 +1,16 @@ package com.kamco.cd.kamcoback.members; +import com.kamco.cd.kamcoback.auth.CustomUserDetails; import com.kamco.cd.kamcoback.auth.JwtTokenProvider; import com.kamco.cd.kamcoback.auth.RefreshTokenService; import com.kamco.cd.kamcoback.config.api.ApiResponseDto; +import com.kamco.cd.kamcoback.members.dto.MembersDto; import com.kamco.cd.kamcoback.members.dto.SignInRequest; +import com.kamco.cd.kamcoback.members.dto.TokenResponse; import com.kamco.cd.kamcoback.members.service.AuthService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -50,80 +54,118 @@ public class AuthController { @Operation(summary = "로그인", description = "사번으로 로그인하여 액세스/리프레시 토큰을 발급.") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "로그인 성공", - content = @Content(schema = @Schema(implementation = TokenResponse.class))), + responseCode = "200", + description = "로그인 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class))), @ApiResponse( - responseCode = "401", - description = "ID 또는 비밀번호 불일치", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))), - @ApiResponse( - responseCode = "400", - description = "미사용 상태", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + responseCode = "401", + description = "로그인 실패 (아이디/비밀번호 오류, 계정잠금 등)", + content = + @Content( + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "아이디 입력 오류", + description = "존재하지 않는 아이디", + value = + """ + { + "code": "LOGIN_ID_NOT_FOUND", + "message": "아이디를 잘못 입력하셨습니다." + } + """), + @ExampleObject( + name = "비밀번호 입력 오류 (4회 이하)", + description = "아이디는 정상, 비밀번호를 여러 번 틀린 경우", + value = + """ + { + "code": "LOGIN_PASSWORD_MISMATCH", + "message": "비밀번호를 잘못 입력하셨습니다." + } + """), + @ExampleObject( + name = "비밀번호 오류 횟수 초과", + description = "비밀번호 5회 이상 오류로 계정 잠김", + value = + """ + { + "code": "LOGIN_PASSWORD_EXCEEDED", + "message": "비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다. 로그인 오류에 대해 관리자에게 문의하시기 바랍니다." + } + """) + })) }) public ApiResponseDto signin( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "로그인 요청 정보", - required = true) - @RequestBody - SignInRequest request, - HttpServletResponse response) { + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "로그인 요청 정보", + required = true) + @RequestBody + SignInRequest request, + HttpServletResponse response) { Authentication authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); String status = authService.getUserStatus(request); - if ("INACTIVE".equals(status)) { - return ApiResponseDto.ok(new TokenResponse(status, null, null)); + + MembersDto.Member member = new MembersDto.Member(); + + // INACTIVE 비활성 상태(새로운 패스워드 입력 해야함), DELETED 탈퇴 + if (!"ACTIVE".equals(status)) { + return ApiResponseDto.ok(new TokenResponse(status, null, null, member)); } String username = authentication.getName(); // UserDetailsService 에서 사용한 username - // 로그인 시간 저장 - authService.saveLogin(UUID.fromString(username)); - String accessToken = jwtTokenProvider.createAccessToken(username); String refreshToken = jwtTokenProvider.createRefreshToken(username); // Redis에 RefreshToken 저장 (TTL = 7일) refreshTokenService.save( - username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); + username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); // HttpOnly + Secure 쿠키에 RefreshToken 저장 ResponseCookie cookie = - ResponseCookie.from(refreshCookieName, refreshToken) - .httpOnly(true) - .secure(refreshCookieSecure) - .path("/") - .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) - .sameSite("Strict") - .build(); + ResponseCookie.from(refreshCookieName, refreshToken) + .httpOnly(true) + .secure(refreshCookieSecure) + .path("/") + .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) + .sameSite("Strict") + .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); - return ApiResponseDto.ok(new TokenResponse(status, accessToken, refreshToken)); + CustomUserDetails user = (CustomUserDetails) authentication.getPrincipal(); + member.setId(user.getMember().getId()); + member.setName(user.getMember().getName()); + member.setEmployeeNo(user.getMember().getEmployeeNo()); + + // 인증 성공 로그인 시간 저장 + authService.saveLogin(UUID.fromString(username)); + + return ApiResponseDto.ok(new TokenResponse(status, accessToken, refreshToken, member)); } @PostMapping("/refresh") @Operation(summary = "토큰 재발급", description = "리프레시 토큰으로 새로운 액세스/리프레시 토큰을 재발급합니다.") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "재발급 성공", - content = @Content(schema = @Schema(implementation = TokenResponse.class))), + responseCode = "200", + description = "재발급 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class))), @ApiResponse( - responseCode = "401", - description = "만료되었거나 유효하지 않은 리프레시 토큰", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + responseCode = "403", + description = "만료되었거나 유효하지 않은 리프레시 토큰", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) public ResponseEntity refresh(String refreshToken, HttpServletResponse response) - throws AccessDeniedException { + throws AccessDeniedException { if (refreshToken == null || !jwtTokenProvider.isValidToken(refreshToken)) { throw new AccessDeniedException("만료되었거나 유효하지 않은 리프레시 토큰 입니다."); } - String username = jwtTokenProvider.getSubject(refreshToken); // Redis에 저장된 RefreshToken과 일치하는지 확인 @@ -137,32 +179,33 @@ public class AuthController { // Redis 갱신 refreshTokenService.save( - username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); + username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); // 쿠키 갱신 ResponseCookie cookie = - ResponseCookie.from(refreshCookieName, newRefreshToken) - .httpOnly(true) - .secure(refreshCookieSecure) - .path("/") - .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) - .sameSite("Strict") - .build(); + ResponseCookie.from(refreshCookieName, newRefreshToken) + .httpOnly(true) + .secure(refreshCookieSecure) + .path("/") + .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) + .sameSite("Strict") + .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); - return ResponseEntity.ok(new TokenResponse("ACTIVE", newAccessToken, newRefreshToken)); + MembersDto.Member member = new MembersDto.Member(); + return ResponseEntity.ok(new TokenResponse("ACTIVE", newAccessToken, newRefreshToken, member)); } @PostMapping("/logout") @Operation(summary = "로그아웃", description = "현재 사용자의 토큰을 무효화(리프레시 토큰 삭제)합니다.") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "로그아웃 성공", - content = @Content(schema = @Schema(implementation = Void.class))) + responseCode = "200", + description = "로그아웃 성공", + content = @Content(schema = @Schema(implementation = Void.class))) }) public ApiResponseDto> logout( - Authentication authentication, HttpServletResponse response) { + Authentication authentication, HttpServletResponse response) { if (authentication != null) { String username = authentication.getName(); // Redis에서 RefreshToken 삭제 @@ -171,19 +214,15 @@ public class AuthController { // 쿠키 삭제 (Max-Age=0) ResponseCookie cookie = - ResponseCookie.from(refreshCookieName, "") - .httpOnly(true) - .secure(refreshCookieSecure) - .path("/") - .maxAge(0) - .sameSite("Strict") - .build(); + ResponseCookie.from(refreshCookieName, "") + .httpOnly(true) + .secure(refreshCookieSecure) + .path("/") + .maxAge(0) + .sameSite("Strict") + .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); return ApiResponseDto.createOK(ResponseEntity.noContent().build()); } - - public record TokenResponse(String status, String accessToken, String refreshToken) { - - } } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java index 6f63553c..2d6a850b 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java @@ -14,6 +14,8 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -27,44 +29,51 @@ import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor public class MembersApiController { + private final AuthenticationManager authenticationManager; private final MembersService membersService; @Operation(summary = "회원정보 목록", description = "회원정보 조회") @ApiResponses( - value = { - @ApiResponse( - responseCode = "200", - description = "검색 성공", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Page.class))), - @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "200", + description = "검색 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Page.class))), + @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @GetMapping public ApiResponseDto> getMemberList( - @ParameterObject MembersDto.SearchReq searchReq) { + @ParameterObject MembersDto.SearchReq searchReq) { return ApiResponseDto.ok(membersService.findByMembers(searchReq)); } - - @Operation(summary = "사용자 비밀번호 변경", description = "사용자 비밀번호 변경") + @Operation( + summary = "사용자 비밀번호 변경", + description = "로그인 성공후 status가 INACTIVE일때 로그인 id를 memberId로 path 생성필요") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "사용자 비밀번호 변경", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Long.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "201", + description = "사용자 비밀번호 변경", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Long.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @PatchMapping("/{memberId}/password") - public ApiResponseDto resetPassword(@PathVariable Long memberId, @RequestBody @Valid MembersDto.InitReq initReq) { + public ApiResponseDto resetPassword( + @PathVariable String memberId, @RequestBody @Valid MembersDto.InitReq initReq) { + + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(memberId, initReq.getTempPassword())); + membersService.resetPassword(memberId, initReq); return ApiResponseDto.createOK(memberId); } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java index d28d39ca..7f6fe46c 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java @@ -1,6 +1,9 @@ package com.kamco.cd.kamcoback.members.dto; +import com.kamco.cd.kamcoback.common.enums.RoleType; +import com.kamco.cd.kamcoback.common.enums.StatusType; import com.kamco.cd.kamcoback.common.utils.interfaces.JsonFormatDttm; +import com.kamco.cd.kamcoback.config.enums.EnumType; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; @@ -22,47 +25,56 @@ public class MembersDto { private Long id; private UUID uuid; private String userRole; + private String userRoleName; private String name; private String userId; private String employeeNo; private String tempPassword; private String status; - @JsonFormatDttm - private ZonedDateTime createdDttm; - @JsonFormatDttm - private ZonedDateTime updatedDttm; - @JsonFormatDttm - private ZonedDateTime firstLoginDttm; - @JsonFormatDttm - private ZonedDateTime lastLoginDttm; + private String statusName; + @JsonFormatDttm private ZonedDateTime createdDttm; + @JsonFormatDttm private ZonedDateTime updatedDttm; + @JsonFormatDttm private ZonedDateTime firstLoginDttm; + @JsonFormatDttm private ZonedDateTime lastLoginDttm; public Basic( - Long id, - UUID uuid, - String userRole, - String name, - String userId, - String employeeNo, - String tempPassword, - String status, - ZonedDateTime createdDttm, - ZonedDateTime updatedDttm, - ZonedDateTime firstLoginDttm, - ZonedDateTime lastLoginDttm - ) { + Long id, + UUID uuid, + String userRole, + String name, + String userId, + String employeeNo, + String tempPassword, + String status, + ZonedDateTime createdDttm, + ZonedDateTime updatedDttm, + ZonedDateTime firstLoginDttm, + ZonedDateTime lastLoginDttm) { this.id = id; this.uuid = uuid; this.userRole = userRole; + this.userRoleName = getUserRoleName(userRole); this.name = name; this.userId = userId; this.employeeNo = employeeNo; this.tempPassword = tempPassword; this.status = status; + this.statusName = getStatusName(status); this.createdDttm = createdDttm; this.updatedDttm = updatedDttm; this.firstLoginDttm = firstLoginDttm; this.lastLoginDttm = lastLoginDttm; } + + private String getUserRoleName(String roleId) { + RoleType type = EnumType.fromId(RoleType.class, roleId); + return type.getText(); + } + + private String getStatusName(String status) { + StatusType type = EnumType.fromId(StatusType.class, status); + return type.getText(); + } } @Getter @@ -71,7 +83,9 @@ public class MembersDto { @AllArgsConstructor public static class SearchReq { - @Schema(description = "전체, 관리자(ROLE_ADMIN), 라벨러(ROLE_LABELER), 검수자(ROLE_REVIEWER)", example = "") + @Schema( + description = "전체, 관리자(ROLE_ADMIN), 라벨러(ROLE_LABELER), 검수자(ROLE_REVIEWER)", + example = "") private String userRole; @Schema(description = "키워드", example = "홍길동") @@ -115,7 +129,7 @@ public class MembersDto { private String employeeNo; public AddReq( - String userRole, String name, String userId, String tempPassword, String employeeNo) { + String userRole, String name, String userId, String tempPassword, String employeeNo) { this.userRole = userRole; this.name = name; this.userId = userId; @@ -160,4 +174,15 @@ public class MembersDto { @NotBlank private String tempPassword; } + + @Getter + @Setter + @AllArgsConstructor + @NoArgsConstructor + public static class Member { + + private Long id; + private String name; + private String employeeNo; + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java b/src/main/java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java index a13992b3..1ad47961 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java @@ -11,10 +11,10 @@ import lombok.ToString; @ToString(exclude = "password") public class SignInRequest { - @Schema(description = "사용자 ID", example = "admin") + @Schema(description = "사용자 ID", example = "admin2") private String username; - @Schema(description = "비밀번호", example = "kamco1234!") + @Schema(description = "비밀번호", example = "Admin2!@#") @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private String password; } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/dto/TokenResponse.java b/src/main/java/com/kamco/cd/kamcoback/members/dto/TokenResponse.java new file mode 100644 index 00000000..d410279e --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/members/dto/TokenResponse.java @@ -0,0 +1,16 @@ +package com.kamco.cd.kamcoback.members.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class TokenResponse { + + private String status; + private String accessToken; + private String refreshToken; + private MembersDto.Member member; +} diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/AdminService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/AdminService.java index da6905d7..dd6f5621 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/AdminService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/AdminService.java @@ -36,7 +36,6 @@ public class AdminService { membersCoreService.updateMembers(uuid, updateReq); } - /** * 관리자 계정 미사용 처리 * diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java index 5433750b..287bc99b 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java @@ -24,16 +24,6 @@ public class AuthService { membersCoreService.saveLogin(uuid); } - /** - * 로그인 실패 저장 - * - * @param uuid - */ - @Transactional - public void loginFail(UUID uuid) { - membersCoreService.loginFail(uuid); - } - /** * 사용자 상태 조회 * diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java index 945d4868..c9611d6d 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java @@ -35,7 +35,8 @@ public class MembersService { * @param initReq */ @Transactional - public void resetPassword(Long id, MembersDto.InitReq initReq) { + public void resetPassword(String id, MembersDto.InitReq initReq) { + if (!isValidPassword(initReq.getPassword())) { throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST); } @@ -43,13 +44,14 @@ public class MembersService { } /** - * 대문자 1개 이상 소문자 1개 이상 숫자 1개 이상 특수문자(!@#$) 1개 이상 + * 영문, 숫자, 특수문자를 모두 포함하여 8~20자 이내의 비밀번호 * * @param password * @return */ private boolean isValidPassword(String password) { - String regex = "^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d)(?=.*[!@#$]).{8,20}$"; - return Pattern.matches(regex, password); + String passwordPattern = + "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-\\[\\]{};':\"\\\\|,.<>/?]).{8,20}$"; + return Pattern.matches(passwordPattern, password); } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java index d1a1b642..77039d5a 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java @@ -1,6 +1,8 @@ package com.kamco.cd.kamcoback.postgres.core; import com.kamco.cd.kamcoback.auth.BCryptSaltGenerator; +import com.kamco.cd.kamcoback.common.enums.StatusType; +import com.kamco.cd.kamcoback.common.utils.UserUtil; import com.kamco.cd.kamcoback.members.dto.MembersDto; import com.kamco.cd.kamcoback.members.dto.MembersDto.AddReq; import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic; @@ -8,7 +10,6 @@ import com.kamco.cd.kamcoback.members.dto.SignInRequest; import com.kamco.cd.kamcoback.members.exception.MemberException.DuplicateMemberException; import com.kamco.cd.kamcoback.members.exception.MemberException.DuplicateMemberException.Field; import com.kamco.cd.kamcoback.members.exception.MemberException.MemberNotFoundException; -import com.kamco.cd.kamcoback.members.exception.MemberException.PasswordNotFoundException; import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository; import java.time.ZonedDateTime; @@ -24,6 +25,7 @@ import org.springframework.stereotype.Service; public class MembersCoreService { private final MembersRepository membersRepository; + private final UserUtil userUtil; /** * 관리자 계정 등록 @@ -48,6 +50,7 @@ public class MembersCoreService { memberEntity.setPassword(hashedPassword); memberEntity.setName(addReq.getName()); memberEntity.setEmployeeNo(addReq.getEmployeeNo()); + memberEntity.setRgstrUidl(userUtil.getId()); return membersRepository.save(memberEntity).getId(); } @@ -60,7 +63,7 @@ public class MembersCoreService { */ public void updateMembers(UUID uuid, MembersDto.UpdateReq updateReq) { MemberEntity memberEntity = - membersRepository.findByUUID(uuid).orElseThrow(MemberNotFoundException::new); + membersRepository.findByUUID(uuid).orElseThrow(MemberNotFoundException::new); if (StringUtils.isNotBlank(updateReq.getName())) { memberEntity.setName(updateReq.getName()); @@ -75,48 +78,47 @@ public class MembersCoreService { memberEntity.setEmployeeNo(updateReq.getEmployeeNo()); } + memberEntity.setUpdtrUid(userUtil.getId()); + membersRepository.save(memberEntity); } - /** - * 관리자 계정 미사용 처리 + * 관리자 계정 삭제 처리 * * @param uuid */ public void deleteAccount(UUID uuid) { MemberEntity memberEntity = - membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); + membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); - memberEntity.setStatus("INACTIVE"); + memberEntity.setStatus(StatusType.DELETED.getId()); memberEntity.setUpdatedDttm(ZonedDateTime.now()); + memberEntity.setUpdtrUid(userUtil.getId()); membersRepository.save(memberEntity); } - /** * 패스워드 변경 * * @param id */ - public void resetPassword(Long id, MembersDto.InitReq initReq) { + public void resetPassword(String id, MembersDto.InitReq initReq) { MemberEntity memberEntity = - membersRepository.findById(id).orElseThrow(() -> new MemberNotFoundException()); - - if (!memberEntity.getTempPassword().equals(initReq.getTempPassword())) { - throw new PasswordNotFoundException(); - } + membersRepository.findByUserId(id).orElseThrow(() -> new MemberNotFoundException()); String salt = - BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim()); + BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim()); // 패스워드 암호화 String hashedPassword = BCrypt.hashpw(initReq.getPassword(), salt); memberEntity.setPassword(hashedPassword); memberEntity.setStatus("ACTIVE"); memberEntity.setUpdatedDttm(ZonedDateTime.now()); + memberEntity.setUpdtrUid(memberEntity.getId()); membersRepository.save(memberEntity); } + // /** @@ -129,6 +131,20 @@ public class MembersCoreService { return membersRepository.findByMembers(searchReq); } + /** + * 사용자 상태 조회 + * + * @param request + * @return + */ + public String getUserStatus(SignInRequest request) { + MemberEntity memberEntity = + membersRepository + .findByUserId(request.getUsername()) + .orElseThrow(MemberNotFoundException::new); + return memberEntity.getStatus(); + } + /** * 최초 로그인 저장 마지막 로그인 저장 * @@ -136,7 +152,7 @@ public class MembersCoreService { */ public void saveLogin(UUID uuid) { MemberEntity memberEntity = - membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); + membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); if (memberEntity.getFirstLoginDttm() == null) { memberEntity.setFirstLoginDttm(ZonedDateTime.now()); @@ -145,33 +161,4 @@ public class MembersCoreService { memberEntity.setLoginFailCount(0); membersRepository.save(memberEntity); } - - /** - * 로그인 실패시 상태 저장 - * - * @param uuid - */ - public void loginFail(UUID uuid) { - MemberEntity memberEntity = - membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); - - int failCnt = memberEntity.getLoginFailCount() + 1; - if (failCnt >= 5) { - memberEntity.setStatus("INACTIVE"); - } - - memberEntity.setLoginFailCount(failCnt); - membersRepository.save(memberEntity); - } - - /** - * 사용자 상태 조회 - * - * @param request - * @return - */ - public String getUserStatus(SignInRequest request) { - MemberEntity memberEntity = membersRepository.findByUserId(request.getUsername()).orElseThrow(MemberNotFoundException::new); - return memberEntity.getStatus(); - } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/CommonCodeEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/CommonCodeEntity.java index 6fe00c0c..f2d829eb 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/CommonCodeEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/CommonCodeEntity.java @@ -2,17 +2,7 @@ package com.kamco.cd.kamcoback.postgres.entity; import com.kamco.cd.kamcoback.code.dto.CommonCodeDto; import com.kamco.cd.kamcoback.postgres.CommonDateEntity; -import jakarta.persistence.CascadeType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; -import jakarta.persistence.Table; +import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import java.time.ZonedDateTime; @@ -21,6 +11,7 @@ import java.util.List; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.Where; @Entity @Getter @@ -60,6 +51,7 @@ public class CommonCodeEntity extends CommonDateEntity { private CommonCodeEntity parent; @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @Where(clause = "deleted = false or deleted is null") private List children = new ArrayList<>(); @Size(max = 255) diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MemberEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MemberEntity.java index 5a4879b0..19f7e4d3 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MemberEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MemberEntity.java @@ -1,5 +1,6 @@ package com.kamco.cd.kamcoback.postgres.entity; +import com.kamco.cd.kamcoback.common.enums.StatusType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -63,7 +64,7 @@ public class MemberEntity { @Size(max = 20) @ColumnDefault("'INACTIVE'") @Column(name = "status", length = 20) - private String status = "INACTIVE"; + private String status = StatusType.INACTIVE.getId(); @NotNull @ColumnDefault("now()") @@ -83,4 +84,10 @@ public class MemberEntity { @Column(name = "login_fail_count") @ColumnDefault("0") private Integer loginFailCount = 0; + + @Column(name = "rgstr_uid") + private Long rgstrUidl; + + @Column(name = "updtr_uid") + private Long updtrUid; } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java index c46e73e2..2180f2dc 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java @@ -20,18 +20,8 @@ public class CommonCodeRepositoryImpl implements CommonCodeRepositoryCustom { @Override public Optional findByCodeId(Long id) { - QCommonCodeEntity child = new QCommonCodeEntity("child"); return Optional.ofNullable( - queryFactory - .selectFrom(commonCodeEntity) - .leftJoin(commonCodeEntity.children, child) - .fetchJoin() - .where( - commonCodeEntity.id.eq(id), - commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull()), - child.deleted.isFalse().or(child.deleted.isNull())) - .orderBy(commonCodeEntity.order.asc(), child.order.asc()) - .fetchOne()); + queryFactory.selectFrom(commonCodeEntity).where(commonCodeEntity.id.eq(id)).fetchOne()); } @Override @@ -41,13 +31,12 @@ public class CommonCodeRepositoryImpl implements CommonCodeRepositoryCustom { queryFactory .selectFrom(commonCodeEntity) .leftJoin(commonCodeEntity.children, child) - .fetchJoin() + .on(child.deleted.isFalse().or(child.deleted.isNull())) .where( commonCodeEntity.parent.isNull(), commonCodeEntity.code.eq(code), commonCodeEntity.used.isTrue(), - commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull()), - child.deleted.isFalse().or(child.deleted.isNull())) + commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull())) .orderBy(child.order.asc()) .fetchOne()); } @@ -61,8 +50,7 @@ public class CommonCodeRepositoryImpl implements CommonCodeRepositoryCustom { .on(child.deleted.isFalse().or(child.deleted.isNull())) .where( commonCodeEntity.parent.isNull(), - commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull()) - ) + commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull())) .orderBy(commonCodeEntity.order.asc(), child.order.asc()) .fetch(); } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryImpl.java index 7e84baec..989bdd7d 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryImpl.java @@ -33,11 +33,11 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom { @Override public boolean existsByUserId(String userId) { return queryFactory - .selectOne() - .from(memberEntity) - .where(memberEntity.userId.eq(userId)) - .fetchFirst() - != null; + .selectOne() + .from(memberEntity) + .where(memberEntity.userId.eq(userId)) + .fetchFirst() + != null; } /** @@ -49,7 +49,7 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom { @Override public Optional findByUserId(String userId) { return Optional.ofNullable( - queryFactory.selectFrom(memberEntity).where(memberEntity.userId.eq(userId)).fetchOne()); + queryFactory.selectFrom(memberEntity).where(memberEntity.userId.eq(userId)).fetchOne()); } /** @@ -68,10 +68,11 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom { String contains = "%" + searchReq.getKeyword() + "%"; builder.and( - memberEntity.name.likeIgnoreCase(contains) - .or(memberEntity.userId.likeIgnoreCase(contains)) - .or(memberEntity.employeeNo.likeIgnoreCase(contains)) - ); + memberEntity + .name + .likeIgnoreCase(contains) + .or(memberEntity.userId.likeIgnoreCase(contains)) + .or(memberEntity.employeeNo.likeIgnoreCase(contains))); } // 권한 @@ -80,35 +81,30 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom { } List content = - queryFactory - .select( - Projections.constructor( - MembersDto.Basic.class, - memberEntity.id, - memberEntity.uuid, - memberEntity.userRole, - memberEntity.name, - memberEntity.userId, - memberEntity.employeeNo, - memberEntity.tempPassword, - memberEntity.status, - memberEntity.createdDttm, - memberEntity.updatedDttm, - memberEntity.firstLoginDttm, - memberEntity.lastLoginDttm - )) - .from(memberEntity) - .where(builder) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .orderBy(memberEntity.createdDttm.desc()) - .fetch(); + queryFactory + .select( + Projections.constructor( + MembersDto.Basic.class, + memberEntity.id, + memberEntity.uuid, + memberEntity.userRole, + memberEntity.name, + memberEntity.userId, + memberEntity.employeeNo, + memberEntity.tempPassword, + memberEntity.status, + memberEntity.createdDttm, + memberEntity.updatedDttm, + memberEntity.firstLoginDttm, + memberEntity.lastLoginDttm)) + .from(memberEntity) + .where(builder) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(memberEntity.createdDttm.desc()) + .fetch(); - long total = - queryFactory - .select(memberEntity) - .from(memberEntity) - .fetchCount(); + long total = queryFactory.select(memberEntity).from(memberEntity).fetchCount(); return new PageImpl<>(content, pageable, total); } @@ -122,6 +118,6 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom { @Override public Optional findByUUID(UUID uuid) { return Optional.ofNullable( - queryFactory.selectFrom(memberEntity).where(memberEntity.uuid.eq(uuid)).fetchOne()); + queryFactory.selectFrom(memberEntity).where(memberEntity.uuid.eq(uuid)).fetchOne()); } } diff --git a/src/main/java/com/kamco/cd/kamcoback/test/TestApiController.java b/src/main/java/com/kamco/cd/kamcoback/test/TestApiController.java new file mode 100644 index 00000000..ca2dfe53 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/test/TestApiController.java @@ -0,0 +1,68 @@ +package com.kamco.cd.kamcoback.test; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.ErrorResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "test api", description = "test api") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/test") +public class TestApiController { + + @Operation(summary = "admin test", description = "admin test api") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "조회", + content = @Content(schema = @Schema(implementation = String.class))), + @ApiResponse( + responseCode = "403", + description = "권한 없음", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/admin") + public String admin() { + return "I am administrator"; + } + + @Operation(summary = "label test", description = "label test api") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "조회", + content = @Content(schema = @Schema(implementation = String.class))), + @ApiResponse( + responseCode = "403", + description = "권한 없음", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/label") + public String label() { + return "Labeling is available."; + } + + @Operation(summary = "review test", description = "review test api") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "조회", + content = @Content(schema = @Schema(implementation = String.class))), + @ApiResponse( + responseCode = "403", + description = "권한 없음", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/review") + public String review() { + return "Review is available."; + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 0b2d19f6..2013e5fb 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -38,8 +38,10 @@ spring: jwt: secret: "kamco_token_9b71e778-19a3-4c1d-97bf-2d687de17d5b" - access-token-validity-in-ms: 86400000 # 1일 - refresh-token-validity-in-ms: 604800000 # 7일 + #access-token-validity-in-ms: 86400000 # 1일 + #refresh-token-validity-in-ms: 604800000 # 7일 + access-token-validity-in-ms: 60000 # 1분 + refresh-token-validity-in-ms: 300000 # 5분 token: refresh-cookie-name: kamco-dev # 개발용 쿠키 이름