Merge pull request 'feat/dev_251201' (#43) from feat/dev_251201 into develop
Reviewed-on: https://kamco.gitea.gs.dabeeo.com/dabeeo/kamco-dabeeo-backoffice/pulls/43
This commit is contained in:
@@ -1,14 +1,17 @@
|
|||||||
package com.kamco.cd.kamcoback.auth;
|
package com.kamco.cd.kamcoback.auth;
|
||||||
|
|
||||||
|
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.entity.MemberEntity;
|
||||||
import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository;
|
import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.mindrot.jbcrypt.BCrypt;
|
import org.mindrot.jbcrypt.BCrypt;
|
||||||
import org.springframework.security.authentication.AuthenticationProvider;
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -16,6 +19,7 @@ import org.springframework.stereotype.Component;
|
|||||||
public class CustomAuthenticationProvider implements AuthenticationProvider {
|
public class CustomAuthenticationProvider implements AuthenticationProvider {
|
||||||
|
|
||||||
private final MembersRepository membersRepository;
|
private final MembersRepository membersRepository;
|
||||||
|
private final UserDetailsService userDetailsService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||||
@@ -26,14 +30,34 @@ public class CustomAuthenticationProvider implements AuthenticationProvider {
|
|||||||
MemberEntity member =
|
MemberEntity member =
|
||||||
membersRepository
|
membersRepository
|
||||||
.findByUserId(username)
|
.findByUserId(username)
|
||||||
.orElseThrow(() -> new BadCredentialsException("ID 또는 비밀번호가 일치하지 않습니다."));
|
.orElseThrow(() -> new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND));
|
||||||
|
|
||||||
// 2. jBCrypt + 커스텀 salt 로 저장된 패스워드 비교
|
// 2. jBCrypt + 커스텀 salt 로 저장된 패스워드 비교
|
||||||
if (!BCrypt.checkpw(rawPassword, member.getPassword())) {
|
if (!BCrypt.checkpw(rawPassword, member.getPassword())) {
|
||||||
throw new BadCredentialsException("ID 또는 비밀번호가 일치하지 않습니다.");
|
// 실패 카운트 저장
|
||||||
|
int cnt = member.getLoginFailCount() + 1;
|
||||||
|
if (cnt >= 5) {
|
||||||
|
member.setStatus("INACTIVE");
|
||||||
|
}
|
||||||
|
member.setLoginFailCount(cnt);
|
||||||
|
membersRepository.save(member);
|
||||||
|
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 인증 성공 → UserDetails 생성
|
// 3. 패스워드 실패 횟수 체크
|
||||||
|
if (member.getLoginFailCount() >= 5) {
|
||||||
|
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_EXCEEDED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 인증 성공 로그인 시간 저장
|
||||||
|
if (member.getFirstLoginDttm() == null) {
|
||||||
|
member.setFirstLoginDttm(ZonedDateTime.now());
|
||||||
|
}
|
||||||
|
member.setLastLoginDttm(ZonedDateTime.now());
|
||||||
|
member.setLoginFailCount(0);
|
||||||
|
membersRepository.save(member);
|
||||||
|
|
||||||
|
// 5. 인증 성공 → UserDetails 생성
|
||||||
CustomUserDetails userDetails = new CustomUserDetails(member);
|
CustomUserDetails userDetails = new CustomUserDetails(member);
|
||||||
|
|
||||||
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public class CustomUserDetails implements UserDetails {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
return member.getStatus().equalsIgnoreCase("ACTIVE");
|
return "ACTIVE".equals(member.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
public MemberEntity getMember() {
|
public MemberEntity getMember() {
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ package com.kamco.cd.kamcoback.code;
|
|||||||
import com.kamco.cd.kamcoback.code.config.CommonCodeCacheManager;
|
import com.kamco.cd.kamcoback.code.config.CommonCodeCacheManager;
|
||||||
import com.kamco.cd.kamcoback.code.dto.CommonCodeDto;
|
import com.kamco.cd.kamcoback.code.dto.CommonCodeDto;
|
||||||
import com.kamco.cd.kamcoback.code.service.CommonCodeService;
|
import com.kamco.cd.kamcoback.code.service.CommonCodeService;
|
||||||
import com.kamco.cd.kamcoback.common.enums.DetectionClassification;
|
import com.kamco.cd.kamcoback.common.utils.CommonCodeUtil;
|
||||||
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
|
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
|
||||||
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto;
|
|
||||||
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.Clazzes;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
@@ -14,8 +12,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
|||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
@@ -36,6 +32,7 @@ public class CommonCodeApiController {
|
|||||||
|
|
||||||
private final CommonCodeService commonCodeService;
|
private final CommonCodeService commonCodeService;
|
||||||
private final CommonCodeCacheManager commonCodeCacheManager;
|
private final CommonCodeCacheManager commonCodeCacheManager;
|
||||||
|
private final CommonCodeUtil commonCodeUtil;
|
||||||
|
|
||||||
@Operation(summary = "목록 조회", description = "모든 공통코드 조회")
|
@Operation(summary = "목록 조회", description = "모든 공통코드 조회")
|
||||||
@ApiResponses(
|
@ApiResponses(
|
||||||
@@ -212,13 +209,22 @@ public class CommonCodeApiController {
|
|||||||
return ApiResponseDto.ok(commonCodeService.findByCode(code));
|
return ApiResponseDto.ok(commonCodeService.findByCode(code));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "변화탐지 분류 코드 목록", description = "변화탐지 분류 코드 목록(공통코드 기반)")
|
||||||
@GetMapping("/clazz")
|
@GetMapping("/clazz")
|
||||||
public ApiResponseDto<List<InferenceResultDto.Clazzes>> getClasses() {
|
public ApiResponseDto<List<CommonCodeDto.Clazzes>> getClasses() {
|
||||||
|
|
||||||
List<Clazzes> list =
|
// List<Clazzes> list =
|
||||||
Arrays.stream(DetectionClassification.values())
|
// Arrays.stream(DetectionClassification.values())
|
||||||
.sorted(Comparator.comparingInt(DetectionClassification::getOrder))
|
// .sorted(Comparator.comparingInt(DetectionClassification::getOrder))
|
||||||
.map(Clazzes::new)
|
// .map(Clazzes::new)
|
||||||
|
// .toList();
|
||||||
|
|
||||||
|
// 변화탐지 clazz API : enum -> 공통코드로 변경
|
||||||
|
List<CommonCodeDto.Clazzes> list =
|
||||||
|
commonCodeUtil.getChildCodesByParentCode("0000").stream()
|
||||||
|
.map(
|
||||||
|
child ->
|
||||||
|
new CommonCodeDto.Clazzes(child.getCode(), child.getName(), child.getOrder()))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
return ApiResponseDto.ok(list);
|
return ApiResponseDto.ok(list);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.kamco.cd.kamcoback.code.dto;
|
package com.kamco.cd.kamcoback.code.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
import com.kamco.cd.kamcoback.common.utils.html.HtmlEscapeDeserializer;
|
import com.kamco.cd.kamcoback.common.utils.html.HtmlEscapeDeserializer;
|
||||||
@@ -71,15 +72,6 @@ public class CommonCodeDto {
|
|||||||
@NotNull private Integer order;
|
@NotNull private Integer order;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public static class OrderReqDetail {
|
|
||||||
@NotNull private Long id;
|
|
||||||
@NotNull private Integer order;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Schema(name = "CommonCode Basic", description = "공통코드 기본 정보")
|
@Schema(name = "CommonCode Basic", description = "공통코드 기본 정보")
|
||||||
@Getter
|
@Getter
|
||||||
public static class Basic {
|
public static class Basic {
|
||||||
@@ -139,4 +131,22 @@ public class CommonCodeDto {
|
|||||||
this.deletedDttm = deletedDttm;
|
this.deletedDttm = deletedDttm;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public static class Clazzes {
|
||||||
|
|
||||||
|
private String code;
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
private Double score;
|
||||||
|
|
||||||
|
private Integer order;
|
||||||
|
|
||||||
|
public Clazzes(String code, String name, Integer order) {
|
||||||
|
this.code = code;
|
||||||
|
this.name = name;
|
||||||
|
this.order = order;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import lombok.Getter;
|
|||||||
@Getter
|
@Getter
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public enum RoleType implements EnumType {
|
public enum RoleType implements EnumType {
|
||||||
ROLE_ADMIN("시스템 관리자"),
|
ROLE_ADMIN("관리자"),
|
||||||
ROLE_LABELER("라벨러"),
|
ROLE_LABELER("라벨러"),
|
||||||
ROLE_REVIEWER("검수자");
|
ROLE_REVIEWER("검수자");
|
||||||
|
|
||||||
@@ -22,4 +22,13 @@ public enum RoleType implements EnumType {
|
|||||||
public String getText() {
|
public String getText() {
|
||||||
return desc;
|
return desc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static RoleType from(String value) {
|
||||||
|
for (RoleType type : values()) {
|
||||||
|
if (type.name().equalsIgnoreCase(value)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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("비활성"),
|
||||||
|
ARCHIVED("탈퇴");
|
||||||
|
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return name();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getText() {
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.kamco.cd.kamcoback.common.exception;
|
package com.kamco.cd.kamcoback.common.exception;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.common.utils.ErrorCode;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
@@ -19,4 +20,9 @@ public class CustomApiException extends RuntimeException {
|
|||||||
this.codeName = codeName;
|
this.codeName = codeName;
|
||||||
this.status = status;
|
this.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CustomApiException(ErrorCode errorCode) {
|
||||||
|
this.codeName = errorCode.getCode();
|
||||||
|
this.status = errorCode.getStatus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package com.kamco.cd.kamcoback.common.utils;
|
package com.kamco.cd.kamcoback.common.utils;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.kamco.cd.kamcoback.code.dto.CommonCodeDto;
|
||||||
import com.kamco.cd.kamcoback.code.dto.CommonCodeDto.Basic;
|
import com.kamco.cd.kamcoback.code.dto.CommonCodeDto.Basic;
|
||||||
import com.kamco.cd.kamcoback.code.service.CommonCodeService;
|
import com.kamco.cd.kamcoback.code.service.CommonCodeService;
|
||||||
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
|
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,6 +23,8 @@ public class CommonCodeUtil {
|
|||||||
|
|
||||||
private final CommonCodeService commonCodeService;
|
private final CommonCodeService commonCodeService;
|
||||||
|
|
||||||
|
@Autowired private ObjectMapper objectMapper;
|
||||||
|
|
||||||
public CommonCodeUtil(CommonCodeService commonCodeService) {
|
public CommonCodeUtil(CommonCodeService commonCodeService) {
|
||||||
this.commonCodeService = commonCodeService;
|
this.commonCodeService = commonCodeService;
|
||||||
}
|
}
|
||||||
@@ -30,7 +36,21 @@ public class CommonCodeUtil {
|
|||||||
*/
|
*/
|
||||||
public List<Basic> getAllCommonCodes() {
|
public List<Basic> getAllCommonCodes() {
|
||||||
try {
|
try {
|
||||||
return commonCodeService.getFindAll();
|
// 캐시에 들어간 이후 꺼낼 때, DTO를 인식하지 못하여 LinkedHashMap으로 리턴됨.
|
||||||
|
// LinkedHashMap인 경우 변환처리, 아닌 경우는 그대로 리턴함.
|
||||||
|
List<?> allCodes = commonCodeService.getFindAll();
|
||||||
|
return allCodes.stream()
|
||||||
|
.map(
|
||||||
|
item -> {
|
||||||
|
if (item instanceof LinkedHashMap<?, ?> map) {
|
||||||
|
return objectMapper.convertValue(map, CommonCodeDto.Basic.class);
|
||||||
|
} else if (item instanceof CommonCodeDto.Basic dto) {
|
||||||
|
return dto;
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Unsupported cache type: " + item.getClass());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("공통코드 전체 조회 중 오류 발생", e);
|
log.error("공통코드 전체 조회 중 오류 발생", e);
|
||||||
return List.of();
|
return List.of();
|
||||||
@@ -111,8 +131,24 @@ public class CommonCodeUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<Basic> allCodes = commonCodeService.getFindAll();
|
// 캐시에 들어간 이후 꺼낼 때, DTO를 인식하지 못하여 LinkedHashMap으로 리턴됨.
|
||||||
return allCodes.stream()
|
// LinkedHashMap인 경우 변환처리, 아닌 경우는 그대로 리턴함.
|
||||||
|
List<?> allCodes = commonCodeService.getFindAll();
|
||||||
|
List<Basic> convertList =
|
||||||
|
allCodes.stream()
|
||||||
|
.map(
|
||||||
|
item -> {
|
||||||
|
if (item instanceof LinkedHashMap<?, ?> map) {
|
||||||
|
return objectMapper.convertValue(map, CommonCodeDto.Basic.class);
|
||||||
|
} else if (item instanceof CommonCodeDto.Basic dto) {
|
||||||
|
return dto;
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Unsupported cache type: " + item.getClass());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return convertList.stream()
|
||||||
.filter(code -> parentCode.equals(code.getCode()))
|
.filter(code -> parentCode.equals(code.getCode()))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.map(Basic::getChildren)
|
.map(Basic::getChildren)
|
||||||
@@ -126,12 +162,12 @@ public class CommonCodeUtil {
|
|||||||
/**
|
/**
|
||||||
* 코드 사용 가능 여부 확인
|
* 코드 사용 가능 여부 확인
|
||||||
*
|
*
|
||||||
* @param parentId 상위 코드 ID
|
* @param parentId 상위 코드 ID -> 1depth 는 null 허용 (파라미터 미필수로 변경)
|
||||||
* @param code 확인할 코드값
|
* @param code 확인할 코드값
|
||||||
* @return 사용 가능 여부 (true: 사용 가능, false: 중복 또는 오류)
|
* @return 사용 가능 여부 (true: 사용 가능, false: 중복 또는 오류)
|
||||||
*/
|
*/
|
||||||
public boolean isCodeAvailable(Long parentId, String code) {
|
public boolean isCodeAvailable(Long parentId, String code) {
|
||||||
if (parentId == null || parentId <= 0 || code == null || code.isEmpty()) {
|
if (parentId <= 0 || code == null || code.isEmpty()) {
|
||||||
log.warn("유효하지 않은 입력: parentId={}, code={}", parentId, code);
|
log.warn("유효하지 않은 입력: parentId={}, code={}", parentId, code);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.kamco.cd.kamcoback.common.utils;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
|
public interface ErrorCode {
|
||||||
|
|
||||||
|
String getCode();
|
||||||
|
|
||||||
|
HttpStatus getStatus();
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.kamco.cd.kamcoback.common.utils;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.auth.CustomUserDetails;
|
||||||
|
import com.kamco.cd.kamcoback.auth.JwtTokenProvider;
|
||||||
|
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserUtil {
|
||||||
|
|
||||||
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 SecurityContext의 Authentication 에서 사용자 ID(Long) 가져오기 - 로그인 된 상태(AccessToken 인증 이후)에서만 사용 가능
|
||||||
|
* - 인증 정보 없으면 null 리턴
|
||||||
|
*/
|
||||||
|
public Long getCurrentUserId() {
|
||||||
|
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
|
||||||
|
.filter(auth -> auth.getPrincipal() instanceof CustomUserDetails)
|
||||||
|
.map(auth -> ((CustomUserDetails) auth.getPrincipal()).getMember().getId())
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 현재 SecurityContext의 Authentication 에서 사용자 UUID 가져오기 - 인증 정보 없으면 null 리턴 */
|
||||||
|
public UUID getCurrentUserUuid() {
|
||||||
|
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
|
||||||
|
.filter(auth -> auth.getPrincipal() instanceof CustomUserDetails)
|
||||||
|
.map(auth -> ((CustomUserDetails) auth.getPrincipal()).getMember().getUuid())
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 현재 로그인한 사용자의 MemberEntity 통째로 가져오기 (Optional) */
|
||||||
|
public Optional<MemberEntity> getCurrentMember() {
|
||||||
|
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
|
||||||
|
.filter(auth -> auth.getPrincipal() instanceof CustomUserDetails)
|
||||||
|
.map(auth -> ((CustomUserDetails) auth.getPrincipal()).getMember());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 현재 로그인한 사용자의 MemberEntity 가져오기 (없으면 예외) */
|
||||||
|
public MemberEntity getCurrentMemberOrThrow() {
|
||||||
|
return getCurrentMember().orElseThrow(() -> new IllegalStateException("인증된 사용자를 찾을 수 없습니다."));
|
||||||
|
// 필요하면 여기서 CustomApiException 으로 바꿔도 됨
|
||||||
|
}
|
||||||
|
|
||||||
|
/** AccessToken / RefreshToken 에서 sub(subject, uuid) 추출 - 토큰 문자열만 있을 때 uuid 가져올 때 사용 */
|
||||||
|
public String getSubjectFromToken(String token) {
|
||||||
|
return jwtTokenProvider.getSubject(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@ public class SecurityConfig {
|
|||||||
"/api/auth/signin",
|
"/api/auth/signin",
|
||||||
"/api/auth/refresh",
|
"/api/auth/refresh",
|
||||||
"/swagger-ui/**",
|
"/swagger-ui/**",
|
||||||
|
"/api/members/{memberId}/password",
|
||||||
"/v3/api-docs/**")
|
"/v3/api-docs/**")
|
||||||
.permitAll()
|
.permitAll()
|
||||||
.anyRequest()
|
.anyRequest()
|
||||||
|
|||||||
@@ -164,6 +164,9 @@ public class ApiResponseDto<T> {
|
|||||||
NOT_FOUND_USER_FOR_EMAIL("이메일로 유저를 찾을 수 없습니다."),
|
NOT_FOUND_USER_FOR_EMAIL("이메일로 유저를 찾을 수 없습니다."),
|
||||||
NOT_FOUND_USER("사용자를 찾을 수 없습니다."),
|
NOT_FOUND_USER("사용자를 찾을 수 없습니다."),
|
||||||
UNPROCESSABLE_ENTITY("이 데이터는 삭제할 수 없습니다."),
|
UNPROCESSABLE_ENTITY("이 데이터는 삭제할 수 없습니다."),
|
||||||
|
LOGIN_ID_NOT_FOUND("아이디를 잘못 입력하셨습니다."),
|
||||||
|
LOGIN_PASSWORD_MISMATCH("비밀번호를 잘못 입력하셨습니다."),
|
||||||
|
LOGIN_PASSWORD_EXCEEDED("비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다.\n로그인 오류에 대해 관리자에게 문의하시기 바랍니다."),
|
||||||
INVALID_EMAIL_TOKEN(
|
INVALID_EMAIL_TOKEN(
|
||||||
"You can only reset your password within 24 hours from when the email was sent.\n"
|
"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"
|
+ "To reset your password again, please submit a new request through \"Forgot"
|
||||||
|
|||||||
@@ -5,4 +5,17 @@ public interface EnumType {
|
|||||||
String getId();
|
String getId();
|
||||||
|
|
||||||
String getText();
|
String getText();
|
||||||
|
|
||||||
|
// code로 text
|
||||||
|
static <E extends Enum<E> & EnumType> E fromId(Class<E> enumClass, String id) {
|
||||||
|
if (id == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (E e : enumClass.getEnumConstants()) {
|
||||||
|
if (id.equalsIgnoreCase(e.getId())) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null; // 못 찾으면 null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import jakarta.validation.Valid;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.PatchMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.PutMapping;
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
@@ -84,17 +83,17 @@ public class AdminApiController {
|
|||||||
schema = @Schema(implementation = MembersDto.UpdateReq.class)))
|
schema = @Schema(implementation = MembersDto.UpdateReq.class)))
|
||||||
@PathVariable
|
@PathVariable
|
||||||
UUID uuid,
|
UUID uuid,
|
||||||
@RequestBody MembersDto.UpdateReq updateReq) {
|
@RequestBody @Valid MembersDto.UpdateReq updateReq) {
|
||||||
adminService.updateMembers(uuid, updateReq);
|
adminService.updateMembers(uuid, updateReq);
|
||||||
return ApiResponseDto.createOK(UUID.randomUUID());
|
return ApiResponseDto.createOK(UUID.randomUUID());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "회원 탈퇴", description = "회원 탈퇴")
|
@Operation(summary = "관리자 계정 미사용 처리", description = "관리자 계정 미사용 처리")
|
||||||
@ApiResponses(
|
@ApiResponses(
|
||||||
value = {
|
value = {
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "201",
|
responseCode = "201",
|
||||||
description = "회원 탈퇴",
|
description = "관리자 계정 미사용 처리",
|
||||||
content =
|
content =
|
||||||
@Content(
|
@Content(
|
||||||
mediaType = "application/json",
|
mediaType = "application/json",
|
||||||
@@ -108,24 +107,4 @@ public class AdminApiController {
|
|||||||
adminService.deleteAccount(uuid);
|
adminService.deleteAccount(uuid);
|
||||||
return ApiResponseDto.createOK(uuid);
|
return ApiResponseDto.createOK(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@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)
|
|
||||||
})
|
|
||||||
@PatchMapping("/{memberId}/password")
|
|
||||||
public ApiResponseDto<Long> resetPassword(@PathVariable Long memberId) {
|
|
||||||
adminService.resetPassword(memberId);
|
|
||||||
return ApiResponseDto.createOK(memberId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import com.kamco.cd.kamcoback.auth.JwtTokenProvider;
|
|||||||
import com.kamco.cd.kamcoback.auth.RefreshTokenService;
|
import com.kamco.cd.kamcoback.auth.RefreshTokenService;
|
||||||
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
|
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
|
||||||
import com.kamco.cd.kamcoback.members.dto.SignInRequest;
|
import com.kamco.cd.kamcoback.members.dto.SignInRequest;
|
||||||
|
import com.kamco.cd.kamcoback.members.service.AuthService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
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.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
@@ -36,6 +38,7 @@ public class AuthController {
|
|||||||
private final AuthenticationManager authenticationManager;
|
private final AuthenticationManager authenticationManager;
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
private final RefreshTokenService refreshTokenService;
|
private final RefreshTokenService refreshTokenService;
|
||||||
|
private final AuthService authService;
|
||||||
|
|
||||||
@Value("${token.refresh-cookie-name}")
|
@Value("${token.refresh-cookie-name}")
|
||||||
private String refreshCookieName;
|
private String refreshCookieName;
|
||||||
@@ -52,8 +55,42 @@ public class AuthController {
|
|||||||
content = @Content(schema = @Schema(implementation = TokenResponse.class))),
|
content = @Content(schema = @Schema(implementation = TokenResponse.class))),
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "401",
|
responseCode = "401",
|
||||||
description = "ID 또는 비밀번호 불일치",
|
description = "로그인 실패 (아이디/비밀번호 오류, 계정잠금 등)",
|
||||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
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<TokenResponse> signin(
|
public ApiResponseDto<TokenResponse> signin(
|
||||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
@@ -62,10 +99,18 @@ public class AuthController {
|
|||||||
@RequestBody
|
@RequestBody
|
||||||
SignInRequest request,
|
SignInRequest request,
|
||||||
HttpServletResponse response) {
|
HttpServletResponse response) {
|
||||||
|
|
||||||
Authentication authentication =
|
Authentication authentication =
|
||||||
authenticationManager.authenticate(
|
authenticationManager.authenticate(
|
||||||
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));
|
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));
|
||||||
|
|
||||||
|
String status = authService.getUserStatus(request);
|
||||||
|
|
||||||
|
// INACTIVE 비활성 상태(새로운 패스워드 입력 해야함), ARCHIVED 탈퇴
|
||||||
|
if (!"ACTIVE".equals(status)) {
|
||||||
|
return ApiResponseDto.ok(new TokenResponse(status, null, null));
|
||||||
|
}
|
||||||
|
|
||||||
String username = authentication.getName(); // UserDetailsService 에서 사용한 username
|
String username = authentication.getName(); // UserDetailsService 에서 사용한 username
|
||||||
|
|
||||||
String accessToken = jwtTokenProvider.createAccessToken(username);
|
String accessToken = jwtTokenProvider.createAccessToken(username);
|
||||||
@@ -86,7 +131,8 @@ public class AuthController {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
|
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
|
||||||
return ApiResponseDto.ok(new TokenResponse(accessToken, refreshToken));
|
|
||||||
|
return ApiResponseDto.ok(new TokenResponse(status, accessToken, refreshToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/refresh")
|
@PostMapping("/refresh")
|
||||||
@@ -133,7 +179,7 @@ public class AuthController {
|
|||||||
.build();
|
.build();
|
||||||
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
|
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
|
||||||
|
|
||||||
return ResponseEntity.ok(new TokenResponse(newAccessToken, newRefreshToken));
|
return ResponseEntity.ok(new TokenResponse("ACTIVE", newAccessToken, newRefreshToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/logout")
|
@PostMapping("/logout")
|
||||||
@@ -166,5 +212,5 @@ public class AuthController {
|
|||||||
return ApiResponseDto.createOK(ResponseEntity.noContent().build());
|
return ApiResponseDto.createOK(ResponseEntity.noContent().build());
|
||||||
}
|
}
|
||||||
|
|
||||||
public record TokenResponse(String accessToken, String refreshToken) {}
|
public record TokenResponse(String status, String accessToken, String refreshToken) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
|||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.util.UUID;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springdoc.core.annotations.ParameterObject;
|
import org.springdoc.core.annotations.ParameterObject;
|
||||||
import org.springframework.data.domain.Page;
|
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.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PatchMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PutMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
@@ -27,6 +29,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class MembersApiController {
|
public class MembersApiController {
|
||||||
|
|
||||||
|
private final AuthenticationManager authenticationManager;
|
||||||
private final MembersService membersService;
|
private final MembersService membersService;
|
||||||
|
|
||||||
@Operation(summary = "회원정보 목록", description = "회원정보 조회")
|
@Operation(summary = "회원정보 목록", description = "회원정보 조회")
|
||||||
@@ -48,24 +51,28 @@ public class MembersApiController {
|
|||||||
return ApiResponseDto.ok(membersService.findByMembers(searchReq));
|
return ApiResponseDto.ok(membersService.findByMembers(searchReq));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "회원정보 수정", description = "회원정보 수정")
|
@Operation(summary = "사용자 비밀번호 변경", description = "사용자 비밀번호 변경")
|
||||||
@ApiResponses(
|
@ApiResponses(
|
||||||
value = {
|
value = {
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "201",
|
responseCode = "201",
|
||||||
description = "회원정보 수정",
|
description = "사용자 비밀번호 변경",
|
||||||
content =
|
content =
|
||||||
@Content(
|
@Content(
|
||||||
mediaType = "application/json",
|
mediaType = "application/json",
|
||||||
schema = @Schema(implementation = UUID.class))),
|
schema = @Schema(implementation = Long.class))),
|
||||||
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
|
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
|
||||||
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
|
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
|
||||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||||
})
|
})
|
||||||
@PutMapping("/{uuid}")
|
@PatchMapping("/{memberId}/password")
|
||||||
public ApiResponseDto<UUID> updateMember(
|
public ApiResponseDto<String> resetPassword(
|
||||||
@PathVariable UUID uuid, @RequestBody MembersDto.UpdateReq updateReq) {
|
@PathVariable String memberId, @RequestBody @Valid MembersDto.InitReq initReq) {
|
||||||
// membersService.updateMember(uuid, updateReq);
|
|
||||||
return ApiResponseDto.createOK(uuid);
|
authenticationManager.authenticate(
|
||||||
|
new UsernamePasswordAuthenticationToken(memberId, initReq.getTempPassword()));
|
||||||
|
|
||||||
|
membersService.resetPassword(memberId, initReq);
|
||||||
|
return ApiResponseDto.createOK(memberId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
package com.kamco.cd.kamcoback.members.dto;
|
|
||||||
|
|
||||||
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class MemberDetails implements UserDetails {
|
|
||||||
|
|
||||||
private final MemberEntity member;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
|
||||||
// TODO: tb_member_role 에서 역할 꺼내서 권한으로 변환하고 싶으면 여기 구현
|
|
||||||
// 예시 (나중에 MemberRoleEntity 보고 수정):
|
|
||||||
// return member.getTbMemberRoles().stream()
|
|
||||||
// .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getRoleName()))
|
|
||||||
// .toList();
|
|
||||||
|
|
||||||
return List.of(); // 일단 빈 권한 리스트
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getPassword() {
|
|
||||||
return member.getPassword(); // 암호화된 비밀번호
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getUsername() {
|
|
||||||
// 로그인 ID 로 무엇을 쓸지 선택
|
|
||||||
// 1) 이메일 로그인:
|
|
||||||
return member.getUserId();
|
|
||||||
|
|
||||||
// 2) 사번으로 로그인하고 싶으면:
|
|
||||||
// return member.getEmployeeNo();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isAccountNonExpired() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isAccountNonLocked() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isCredentialsNonExpired() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isEnabled() {
|
|
||||||
// status 가 ACTIVE 일 때만 로그인 허용
|
|
||||||
return "ACTIVE".equalsIgnoreCase(member.getStatus());
|
|
||||||
}
|
|
||||||
|
|
||||||
public MemberEntity getMember() {
|
|
||||||
return member;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
package com.kamco.cd.kamcoback.members.dto;
|
package com.kamco.cd.kamcoback.members.dto;
|
||||||
|
|
||||||
import com.kamco.cd.kamcoback.common.enums.RoleType;
|
import com.kamco.cd.kamcoback.common.enums.RoleType;
|
||||||
import com.kamco.cd.kamcoback.common.utils.interfaces.EnumValid;
|
import com.kamco.cd.kamcoback.common.enums.StatusType;
|
||||||
import com.kamco.cd.kamcoback.common.utils.interfaces.JsonFormatDttm;
|
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 io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.Size;
|
import jakarta.validation.constraints.Size;
|
||||||
@@ -23,33 +24,58 @@ public class MembersDto {
|
|||||||
|
|
||||||
private Long id;
|
private Long id;
|
||||||
private UUID uuid;
|
private UUID uuid;
|
||||||
private String employeeNo;
|
private String userRole;
|
||||||
|
private String userRoleName;
|
||||||
private String name;
|
private String name;
|
||||||
private String email;
|
private String userId;
|
||||||
|
private String employeeNo;
|
||||||
|
private String tempPassword;
|
||||||
private String status;
|
private String status;
|
||||||
private String roleName;
|
private String statusName;
|
||||||
@JsonFormatDttm private ZonedDateTime createdDttm;
|
@JsonFormatDttm private ZonedDateTime createdDttm;
|
||||||
@JsonFormatDttm private ZonedDateTime updatedDttm;
|
@JsonFormatDttm private ZonedDateTime updatedDttm;
|
||||||
|
@JsonFormatDttm private ZonedDateTime firstLoginDttm;
|
||||||
|
@JsonFormatDttm private ZonedDateTime lastLoginDttm;
|
||||||
|
|
||||||
public Basic(
|
public Basic(
|
||||||
Long id,
|
Long id,
|
||||||
UUID uuid,
|
UUID uuid,
|
||||||
String employeeNo,
|
String userRole,
|
||||||
|
String userRoleName,
|
||||||
String name,
|
String name,
|
||||||
String email,
|
String userId,
|
||||||
|
String employeeNo,
|
||||||
|
String tempPassword,
|
||||||
String status,
|
String status,
|
||||||
String roleName,
|
String statusName,
|
||||||
ZonedDateTime createdDttm,
|
ZonedDateTime createdDttm,
|
||||||
ZonedDateTime updatedDttm) {
|
ZonedDateTime updatedDttm,
|
||||||
|
ZonedDateTime firstLoginDttm,
|
||||||
|
ZonedDateTime lastLoginDttm) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.uuid = uuid;
|
this.uuid = uuid;
|
||||||
this.employeeNo = employeeNo;
|
this.userRole = userRole;
|
||||||
|
this.userRoleName = getUserRoleName(userRole);
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.email = email;
|
this.userId = userId;
|
||||||
|
this.employeeNo = employeeNo;
|
||||||
|
this.tempPassword = tempPassword;
|
||||||
this.status = status;
|
this.status = status;
|
||||||
this.roleName = roleName;
|
this.statusName = getStatusName(status);
|
||||||
this.createdDttm = createdDttm;
|
this.createdDttm = createdDttm;
|
||||||
this.updatedDttm = updatedDttm;
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,21 +85,14 @@ public class MembersDto {
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public static class SearchReq {
|
public static class SearchReq {
|
||||||
|
|
||||||
@Schema(description = "이름(name), 이메일(email), 사번(employeeNo)", example = "name")
|
@Schema(
|
||||||
private String field;
|
description = "전체, 관리자(ROLE_ADMIN), 라벨러(ROLE_LABELER), 검수자(ROLE_REVIEWER)",
|
||||||
|
example = "")
|
||||||
|
private String userRole;
|
||||||
|
|
||||||
@Schema(description = "키워드", example = "홍길동")
|
@Schema(description = "키워드", example = "홍길동")
|
||||||
private String keyword;
|
private String keyword;
|
||||||
|
|
||||||
@Schema(description = "라벨러 포함 여부", example = "true")
|
|
||||||
private boolean labeler = true;
|
|
||||||
|
|
||||||
@Schema(description = "검수자 포함 여부", example = "true")
|
|
||||||
private boolean reviewer = true;
|
|
||||||
|
|
||||||
@Schema(description = "운영자 포함 여부", example = "true")
|
|
||||||
private boolean admin = true;
|
|
||||||
|
|
||||||
// 페이징 파라미터
|
// 페이징 파라미터
|
||||||
@Schema(description = "페이지 번호 (0부터 시작) ", example = "0")
|
@Schema(description = "페이지 번호 (0부터 시작) ", example = "0")
|
||||||
private int page = 0;
|
private int page = 0;
|
||||||
@@ -146,29 +165,15 @@ public class MembersDto {
|
|||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public static class RolesDto {
|
public static class InitReq {
|
||||||
|
|
||||||
@Schema(description = "UUID", example = "4e89e487-c828-4a34-a7fc-0d5b0e3b53b5")
|
@Schema(description = "변경 패스워드", example = "")
|
||||||
private UUID uuid;
|
@Size(max = 255)
|
||||||
|
|
||||||
@Schema(description = "역할 ROLE_ADMIN, ROLE_LABELER, ROLE_REVIEWER", example = "ROLE_ADMIN")
|
|
||||||
@EnumValid(enumClass = RoleType.class)
|
|
||||||
private String roleName;
|
|
||||||
|
|
||||||
public RolesDto(UUID uuid, String roleName) {
|
|
||||||
this.uuid = uuid;
|
|
||||||
this.roleName = roleName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public static class StatusDto {
|
|
||||||
|
|
||||||
@Schema(description = "변경할 상태값 ACTIVE, INACTIVE, ARCHIVED", example = "ACTIVE")
|
|
||||||
@NotBlank
|
@NotBlank
|
||||||
private String status;
|
private String password;
|
||||||
|
|
||||||
|
@Schema(description = "초기 패스워드", example = "")
|
||||||
|
@NotBlank
|
||||||
|
private String tempPassword;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import lombok.ToString;
|
|||||||
@ToString(exclude = "password")
|
@ToString(exclude = "password")
|
||||||
public class SignInRequest {
|
public class SignInRequest {
|
||||||
|
|
||||||
@Schema(description = "사용자 ID", example = "admin")
|
@Schema(description = "사용자 ID", example = "admin2")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
@Schema(description = "비밀번호", example = "kamco1234!")
|
@Schema(description = "비밀번호", example = "Admin2!@#")
|
||||||
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
|
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
|
||||||
private String password;
|
private String password;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,4 +35,15 @@ public class MemberException {
|
|||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class PasswordNotFoundException extends RuntimeException {
|
||||||
|
|
||||||
|
public PasswordNotFoundException() {
|
||||||
|
super("Password not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
public PasswordNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package com.kamco.cd.kamcoback.members.service;
|
package com.kamco.cd.kamcoback.members.service;
|
||||||
|
|
||||||
import com.kamco.cd.kamcoback.auth.BCryptSaltGenerator;
|
|
||||||
import com.kamco.cd.kamcoback.members.dto.MembersDto;
|
import com.kamco.cd.kamcoback.members.dto.MembersDto;
|
||||||
import com.kamco.cd.kamcoback.postgres.core.MembersCoreService;
|
import com.kamco.cd.kamcoback.postgres.core.MembersCoreService;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.mindrot.jbcrypt.BCrypt;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -17,69 +15,34 @@ public class AdminService {
|
|||||||
private final MembersCoreService membersCoreService;
|
private final MembersCoreService membersCoreService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회원가입
|
* 관리자 계정 등록
|
||||||
*
|
*
|
||||||
* @param addReq
|
* @param addReq
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public Long saveMember(MembersDto.AddReq addReq) {
|
public Long saveMember(MembersDto.AddReq addReq) {
|
||||||
// salt 생성, 사번이 salt
|
|
||||||
String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(addReq.getUserId().trim());
|
|
||||||
|
|
||||||
// 패스워드 암호화, 초기 패스워드 고정
|
|
||||||
String hashedPassword = BCrypt.hashpw(addReq.getTempPassword(), salt);
|
|
||||||
addReq.setTempPassword(hashedPassword);
|
|
||||||
return membersCoreService.saveMembers(addReq);
|
return membersCoreService.saveMembers(addReq);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 관리자 계정 수정
|
||||||
|
*
|
||||||
|
* @param uuid
|
||||||
|
* @param updateReq
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
public void updateMembers(UUID uuid, MembersDto.UpdateReq updateReq) {
|
public void updateMembers(UUID uuid, MembersDto.UpdateReq updateReq) {
|
||||||
membersCoreService.updateMembers(uuid, updateReq);
|
membersCoreService.updateMembers(uuid, updateReq);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 역할 추가
|
* 관리자 계정 미사용 처리
|
||||||
*
|
|
||||||
* @param rolesDto
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public void saveRoles(MembersDto.RolesDto rolesDto) {
|
|
||||||
// membersCoreService.saveRoles(rolesDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 역할 삭제
|
|
||||||
*
|
|
||||||
* @param rolesDto
|
|
||||||
*/
|
|
||||||
public void deleteRoles(MembersDto.RolesDto rolesDto) {
|
|
||||||
// membersCoreService.deleteRoles(rolesDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 역할 수정
|
|
||||||
*
|
|
||||||
* @param statusDto
|
|
||||||
*/
|
|
||||||
public void updateStatus(UUID uuid, MembersDto.StatusDto statusDto) {
|
|
||||||
// membersCoreService.updateStatus(uuid, statusDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 회원 탈퇴
|
|
||||||
*
|
*
|
||||||
* @param uuid
|
* @param uuid
|
||||||
*/
|
*/
|
||||||
|
@Transactional
|
||||||
public void deleteAccount(UUID uuid) {
|
public void deleteAccount(UUID uuid) {
|
||||||
// membersCoreService.deleteAccount(uuid);
|
membersCoreService.deleteAccount(uuid);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 패스워드 초기화
|
|
||||||
*
|
|
||||||
* @param id
|
|
||||||
*/
|
|
||||||
public void resetPassword(Long id) {
|
|
||||||
// membersCoreService.resetPassword(id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.kamco.cd.kamcoback.members.service;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.members.dto.SignInRequest;
|
||||||
|
import com.kamco.cd.kamcoback.postgres.core.MembersCoreService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class AuthService {
|
||||||
|
|
||||||
|
private final MembersCoreService membersCoreService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 상태 조회
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String getUserStatus(SignInRequest request) {
|
||||||
|
return membersCoreService.getUserStatus(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
package com.kamco.cd.kamcoback.members.service;
|
package com.kamco.cd.kamcoback.members.service;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.common.exception.CustomApiException;
|
||||||
import com.kamco.cd.kamcoback.members.dto.MembersDto;
|
import com.kamco.cd.kamcoback.members.dto.MembersDto;
|
||||||
import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic;
|
import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic;
|
||||||
import com.kamco.cd.kamcoback.postgres.core.MembersCoreService;
|
import com.kamco.cd.kamcoback.postgres.core.MembersCoreService;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -23,38 +25,23 @@ public class MembersService {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public Page<Basic> findByMembers(MembersDto.SearchReq searchReq) {
|
public Page<Basic> findByMembers(MembersDto.SearchReq searchReq) {
|
||||||
return null; // membersCoreService.findByMembers(searchReq);
|
return membersCoreService.findByMembers(searchReq);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회원정보 수정
|
* 패스워드 사용자 변경
|
||||||
*
|
*
|
||||||
* @param uuid
|
* @param id
|
||||||
* @param updateReq
|
* @param initReq
|
||||||
*/
|
*/
|
||||||
// public void updateMember(UUID uuid, MembersDto.UpdateReq updateReq) {
|
@Transactional
|
||||||
//
|
public void resetPassword(String id, MembersDto.InitReq initReq) {
|
||||||
// if (StringUtils.isNotBlank(updateReq.getPassword())) {
|
|
||||||
//
|
if (!isValidPassword(initReq.getPassword())) {
|
||||||
// if (!this.isValidPassword(updateReq.getPassword())) {
|
throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST);
|
||||||
// throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST);
|
}
|
||||||
// }
|
membersCoreService.resetPassword(id, initReq);
|
||||||
//
|
}
|
||||||
// if (StringUtils.isBlank(updateReq.getEmployeeNo())) {
|
|
||||||
// throw new CustomApiException("BAD_REQUEST", HttpStatus.BAD_REQUEST);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // salt 생성, 사번이 salt
|
|
||||||
// String salt =
|
|
||||||
// BCryptSaltGenerator.generateSaltWithEmployeeNo(updateReq.getEmployeeNo().trim());
|
|
||||||
//
|
|
||||||
// // 패스워드 암호화, 초기 패스워드 고정
|
|
||||||
// String hashedPassword = BCrypt.hashpw(updateReq.getPassword(), salt);
|
|
||||||
// updateReq.setPassword(hashedPassword);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// membersCoreService.updateMembers(uuid, updateReq);
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 대문자 1개 이상 소문자 1개 이상 숫자 1개 이상 특수문자(!@#$) 1개 이상
|
* 대문자 1개 이상 소문자 1개 이상 숫자 1개 이상 특수문자(!@#$) 1개 이상
|
||||||
@@ -63,7 +50,8 @@ public class MembersService {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
private boolean isValidPassword(String password) {
|
private boolean isValidPassword(String password) {
|
||||||
String regex = "^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d)(?=.*[!@#$]).{8,20}$";
|
String passwordPattern =
|
||||||
return Pattern.matches(regex, password);
|
"^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-\\[\\]{};':\"\\\\|,.<>/?]).{8,20}$";
|
||||||
|
return Pattern.matches(passwordPattern, password);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
package com.kamco.cd.kamcoback.postgres.core;
|
package com.kamco.cd.kamcoback.postgres.core;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.auth.BCryptSaltGenerator;
|
||||||
import com.kamco.cd.kamcoback.members.dto.MembersDto;
|
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.AddReq;
|
||||||
|
import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic;
|
||||||
|
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;
|
||||||
import com.kamco.cd.kamcoback.members.exception.MemberException.DuplicateMemberException.Field;
|
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.MemberNotFoundException;
|
||||||
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity;
|
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity;
|
||||||
import com.kamco.cd.kamcoback.postgres.repository.members.MembersArchivedRepository;
|
|
||||||
import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository;
|
import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.mindrot.jbcrypt.BCrypt;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -18,10 +23,9 @@ import org.springframework.stereotype.Service;
|
|||||||
public class MembersCoreService {
|
public class MembersCoreService {
|
||||||
|
|
||||||
private final MembersRepository membersRepository;
|
private final MembersRepository membersRepository;
|
||||||
private final MembersArchivedRepository memberArchivedRepository;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회원가입
|
* 관리자 계정 등록
|
||||||
*
|
*
|
||||||
* @param addReq
|
* @param addReq
|
||||||
* @return
|
* @return
|
||||||
@@ -30,11 +34,17 @@ public class MembersCoreService {
|
|||||||
if (membersRepository.existsByUserId(addReq.getUserId())) {
|
if (membersRepository.existsByUserId(addReq.getUserId())) {
|
||||||
throw new DuplicateMemberException(Field.USER_ID, addReq.getUserId());
|
throw new DuplicateMemberException(Field.USER_ID, addReq.getUserId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// salt 생성, 사번이 salt
|
||||||
|
String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(addReq.getEmployeeNo().trim());
|
||||||
|
// 패스워드 암호화, 초기 패스워드 고정
|
||||||
|
String hashedPassword = BCrypt.hashpw(addReq.getTempPassword(), salt);
|
||||||
|
|
||||||
MemberEntity memberEntity = new MemberEntity();
|
MemberEntity memberEntity = new MemberEntity();
|
||||||
memberEntity.setUserId(addReq.getUserId());
|
memberEntity.setUserId(addReq.getUserId());
|
||||||
memberEntity.setUserRole(addReq.getUserRole());
|
memberEntity.setUserRole(addReq.getUserRole());
|
||||||
memberEntity.setTempPassword(addReq.getTempPassword());
|
memberEntity.setTempPassword(addReq.getTempPassword()); // 임시 패스워드는 암호화 하지 않음
|
||||||
memberEntity.setPassword(addReq.getTempPassword());
|
memberEntity.setPassword(hashedPassword);
|
||||||
memberEntity.setName(addReq.getName());
|
memberEntity.setName(addReq.getName());
|
||||||
memberEntity.setEmployeeNo(addReq.getEmployeeNo());
|
memberEntity.setEmployeeNo(addReq.getEmployeeNo());
|
||||||
|
|
||||||
@@ -42,7 +52,7 @@ public class MembersCoreService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회원정보 수정
|
* 관리자 계정 수정
|
||||||
*
|
*
|
||||||
* @param uuid
|
* @param uuid
|
||||||
* @param updateReq
|
* @param updateReq
|
||||||
@@ -55,9 +65,9 @@ public class MembersCoreService {
|
|||||||
memberEntity.setName(updateReq.getName());
|
memberEntity.setName(updateReq.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 임시 패스워드는 암호화 하지 않음
|
||||||
if (StringUtils.isNotBlank(updateReq.getTempPassword())) {
|
if (StringUtils.isNotBlank(updateReq.getTempPassword())) {
|
||||||
memberEntity.setTempPassword(updateReq.getTempPassword());
|
memberEntity.setTempPassword(updateReq.getTempPassword());
|
||||||
memberEntity.setPassword(updateReq.getTempPassword());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(memberEntity.getEmployeeNo())) {
|
if (StringUtils.isNotBlank(memberEntity.getEmployeeNo())) {
|
||||||
@@ -66,123 +76,41 @@ public class MembersCoreService {
|
|||||||
|
|
||||||
membersRepository.save(memberEntity);
|
membersRepository.save(memberEntity);
|
||||||
}
|
}
|
||||||
//
|
|
||||||
// /**
|
/**
|
||||||
// * 역할 추가
|
* 관리자 계정 미사용 처리
|
||||||
// *
|
*
|
||||||
// * @param rolesDto
|
* @param uuid
|
||||||
// */
|
*/
|
||||||
// public void saveRoles(MembersDto.RolesDto rolesDto) {
|
public void deleteAccount(UUID uuid) {
|
||||||
//
|
MemberEntity memberEntity =
|
||||||
// MemberEntity memberEntity =
|
membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException());
|
||||||
// membersRepository
|
|
||||||
// .findByUUID(rolesDto.getUuid())
|
memberEntity.setStatus("INACTIVE");
|
||||||
// .orElseThrow(() -> new MemberNotFoundException());
|
memberEntity.setUpdatedDttm(ZonedDateTime.now());
|
||||||
//
|
membersRepository.save(memberEntity);
|
||||||
// if (memberRoleRepository.findByUuidAndRoleName(rolesDto)) {
|
}
|
||||||
// throw new MemberException.DuplicateMemberException(
|
|
||||||
// MemberException.DuplicateMemberException.Field.DEFAULT, "중복된 역할이 있습니다.");
|
/**
|
||||||
// }
|
* 패스워드 변경
|
||||||
//
|
*
|
||||||
// MemberRoleEntityId memberRoleEntityId = new MemberRoleEntityId();
|
* @param id
|
||||||
// memberRoleEntityId.setMemberUuid(rolesDto.getUuid());
|
*/
|
||||||
// memberRoleEntityId.setRoleName(rolesDto.getRoleName());
|
public void resetPassword(String id, MembersDto.InitReq initReq) {
|
||||||
//
|
MemberEntity memberEntity =
|
||||||
// MemberRoleEntity memberRoleEntity = new MemberRoleEntity();
|
membersRepository.findByUserId(id).orElseThrow(() -> new MemberNotFoundException());
|
||||||
// memberRoleEntity.setId(memberRoleEntityId);
|
|
||||||
// memberRoleEntity.setMemberUuid(memberEntity);
|
String salt =
|
||||||
// memberRoleEntity.setCreatedDttm(ZonedDateTime.now());
|
BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim());
|
||||||
// memberRoleRepository.save(memberRoleEntity);
|
// 패스워드 암호화
|
||||||
// }
|
String hashedPassword = BCrypt.hashpw(initReq.getPassword(), salt);
|
||||||
//
|
|
||||||
// /**
|
memberEntity.setPassword(hashedPassword);
|
||||||
// * 역할 삭제
|
memberEntity.setStatus("ACTIVE");
|
||||||
// *
|
memberEntity.setUpdatedDttm(ZonedDateTime.now());
|
||||||
// * @param rolesDto
|
membersRepository.save(memberEntity);
|
||||||
// */
|
}
|
||||||
// public void deleteRoles(MembersDto.RolesDto rolesDto) {
|
|
||||||
// MemberEntity memberEntity =
|
|
||||||
// membersRepository
|
|
||||||
// .findByUUID(rolesDto.getUuid())
|
|
||||||
// .orElseThrow(() -> new MemberNotFoundException());
|
|
||||||
//
|
|
||||||
// MemberRoleEntityId memberRoleEntityId = new MemberRoleEntityId();
|
|
||||||
// memberRoleEntityId.setMemberUuid(rolesDto.getUuid());
|
|
||||||
// memberRoleEntityId.setRoleName(rolesDto.getRoleName());
|
|
||||||
//
|
|
||||||
// MemberRoleEntity memberRoleEntity = new MemberRoleEntity();
|
|
||||||
// memberRoleEntity.setId(memberRoleEntityId);
|
|
||||||
// memberRoleEntity.setMemberUuid(memberEntity);
|
|
||||||
//
|
|
||||||
// memberRoleRepository.delete(memberRoleEntity);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * 상태 수정
|
|
||||||
// *
|
|
||||||
// * @param statusDto
|
|
||||||
// */
|
|
||||||
// public void updateStatus(UUID uuid, MembersDto.StatusDto statusDto) {
|
|
||||||
// MemberEntity memberEntity =
|
|
||||||
// membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException());
|
|
||||||
//
|
|
||||||
// memberEntity.setStatus(statusDto.getStatus());
|
|
||||||
// memberEntity.setUpdatedDttm(ZonedDateTime.now());
|
|
||||||
// membersRepository.save(memberEntity);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * 회원 탈퇴
|
|
||||||
// *
|
|
||||||
// * @param uuid
|
|
||||||
// */
|
|
||||||
// public void deleteAccount(UUID uuid) {
|
|
||||||
// MemberEntity memberEntity =
|
|
||||||
// membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException());
|
|
||||||
//
|
|
||||||
// MemberArchivedEntityId memberArchivedEntityId = new MemberArchivedEntityId();
|
|
||||||
// memberArchivedEntityId.setUserId(memberEntity.getId());
|
|
||||||
// memberArchivedEntityId.setUuid(memberEntity.getUuid());
|
|
||||||
//
|
|
||||||
// MemberArchivedEntity memberArchivedEntity = new MemberArchivedEntity();
|
|
||||||
// memberArchivedEntity.setId(memberArchivedEntityId);
|
|
||||||
// memberArchivedEntity.setEmployeeNo(memberEntity.getEmployeeNo());
|
|
||||||
// memberArchivedEntity.setName(memberEntity.getName());
|
|
||||||
// memberArchivedEntity.setPassword(memberEntity.getPassword());
|
|
||||||
// memberArchivedEntity.setEmail(memberEntity.getEmail());
|
|
||||||
// memberArchivedEntity.setStatus(memberEntity.getStatus());
|
|
||||||
// memberArchivedEntity.setCreatedDttm(memberEntity.getCreatedDttm());
|
|
||||||
// memberArchivedEntity.setArchivedDttm(ZonedDateTime.now());
|
|
||||||
// memberArchivedRepository.save(memberArchivedEntity);
|
|
||||||
//
|
|
||||||
// memberEntity.setStatus("ARCHIVED");
|
|
||||||
// memberEntity.setName("**********");
|
|
||||||
// memberEntity.setEmployeeNo("**********");
|
|
||||||
// memberEntity.setPassword("**********");
|
|
||||||
// memberEntity.setEmail("**********");
|
|
||||||
// memberEntity.setUpdatedDttm(ZonedDateTime.now());
|
|
||||||
// membersRepository.save(memberEntity);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * 패스워드 초기화
|
|
||||||
// *
|
|
||||||
// * @param id
|
|
||||||
// */
|
|
||||||
// public void resetPassword(Long id) {
|
|
||||||
// MemberEntity memberEntity =
|
|
||||||
// membersRepository.findById(id).orElseThrow(() -> new MemberNotFoundException());
|
|
||||||
//
|
|
||||||
// String salt =
|
|
||||||
// BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim());
|
|
||||||
// // 패스워드 암호화, 초기 패스워드 고정
|
|
||||||
// String hashedPassword = BCrypt.hashpw(password, salt);
|
|
||||||
//
|
|
||||||
// memberEntity.setPassword(hashedPassword);
|
|
||||||
// memberEntity.setStatus("INACTIVE");
|
|
||||||
// memberEntity.setUpdatedDttm(ZonedDateTime.now());
|
|
||||||
// membersRepository.save(memberEntity);
|
|
||||||
// }
|
|
||||||
//
|
//
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -191,7 +119,21 @@ public class MembersCoreService {
|
|||||||
* @param searchReq
|
* @param searchReq
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
// public Page<Basic> findByMembers(MembersDto.SearchReq searchReq) {
|
public Page<Basic> findByMembers(MembersDto.SearchReq searchReq) {
|
||||||
// return membersRepository.findByMembers(searchReq);
|
return membersRepository.findByMembers(searchReq);
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 상태 조회
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String getUserStatus(SignInRequest request) {
|
||||||
|
MemberEntity memberEntity =
|
||||||
|
membersRepository
|
||||||
|
.findByUserId(request.getUsername())
|
||||||
|
.orElseThrow(MemberNotFoundException::new);
|
||||||
|
return memberEntity.getStatus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,4 +73,14 @@ public class MemberEntity {
|
|||||||
@ColumnDefault("now()")
|
@ColumnDefault("now()")
|
||||||
@Column(name = "updated_dttm")
|
@Column(name = "updated_dttm")
|
||||||
private ZonedDateTime updatedDttm = ZonedDateTime.now();
|
private ZonedDateTime updatedDttm = ZonedDateTime.now();
|
||||||
|
|
||||||
|
@Column(name = "first_login_dttm")
|
||||||
|
private ZonedDateTime firstLoginDttm;
|
||||||
|
|
||||||
|
@Column(name = "last_login_dttm")
|
||||||
|
private ZonedDateTime lastLoginDttm;
|
||||||
|
|
||||||
|
@Column(name = "login_fail_count")
|
||||||
|
@ColumnDefault("0")
|
||||||
|
private Integer loginFailCount = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,11 +58,10 @@ public class CommonCodeRepositoryImpl implements CommonCodeRepositoryCustom {
|
|||||||
return queryFactory
|
return queryFactory
|
||||||
.selectFrom(commonCodeEntity)
|
.selectFrom(commonCodeEntity)
|
||||||
.leftJoin(commonCodeEntity.children, child)
|
.leftJoin(commonCodeEntity.children, child)
|
||||||
.fetchJoin()
|
.on(child.deleted.isFalse().or(child.deleted.isNull()))
|
||||||
.where(
|
.where(
|
||||||
commonCodeEntity.parent.isNull(),
|
commonCodeEntity.parent.isNull(),
|
||||||
commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull()),
|
commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull()))
|
||||||
child.deleted.isFalse().or(child.deleted.isNull()))
|
|
||||||
.orderBy(commonCodeEntity.order.asc(), child.order.asc())
|
.orderBy(commonCodeEntity.order.asc(), child.order.asc())
|
||||||
.fetch();
|
.fetch();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
package com.kamco.cd.kamcoback.postgres.repository.members;
|
package com.kamco.cd.kamcoback.postgres.repository.members;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.members.dto.MembersDto;
|
||||||
|
import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic;
|
||||||
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity;
|
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
|
||||||
public interface MembersRepositoryCustom {
|
public interface MembersRepositoryCustom {
|
||||||
|
|
||||||
boolean existsByUserId(String userId);
|
boolean existsByUserId(String userId);
|
||||||
|
|
||||||
Optional<MemberEntity> findByUserId(String employeeNo);
|
Optional<MemberEntity> findByUserId(String userId);
|
||||||
|
|
||||||
Optional<MemberEntity> findByUUID(UUID uuid);
|
Optional<MemberEntity> findByUUID(UUID uuid);
|
||||||
//
|
|
||||||
// Page<Basic> findByMembers(MembersDto.SearchReq searchReq);
|
|
||||||
//
|
|
||||||
|
|
||||||
//
|
Page<Basic> findByMembers(MembersDto.SearchReq searchReq);
|
||||||
// Optional<MemberEntity> findByEmployeeNo(String employeeNo);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
package com.kamco.cd.kamcoback.postgres.repository.members;
|
package com.kamco.cd.kamcoback.postgres.repository.members;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.members.dto.MembersDto;
|
||||||
|
import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic;
|
||||||
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity;
|
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity;
|
||||||
import com.kamco.cd.kamcoback.postgres.entity.QMemberEntity;
|
import com.kamco.cd.kamcoback.postgres.entity.QMemberEntity;
|
||||||
|
import com.querydsl.core.BooleanBuilder;
|
||||||
|
import com.querydsl.core.types.Projections;
|
||||||
import com.querydsl.jpa.impl.JPAQueryFactory;
|
import com.querydsl.jpa.impl.JPAQueryFactory;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageImpl;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
@@ -31,86 +40,81 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom {
|
|||||||
!= null;
|
!= null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 조회 user id
|
||||||
|
*
|
||||||
|
* @param userId
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Optional<MemberEntity> findByUserId(String userId) {
|
public Optional<MemberEntity> findByUserId(String userId) {
|
||||||
return Optional.ofNullable(
|
return Optional.ofNullable(
|
||||||
queryFactory.selectFrom(memberEntity).where(memberEntity.userId.eq(userId)).fetchOne());
|
queryFactory.selectFrom(memberEntity).where(memberEntity.userId.eq(userId)).fetchOne());
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
/**
|
||||||
// * 회원정보 목록 조회
|
* 회원정보 목록 조회
|
||||||
// *
|
*
|
||||||
// * @param searchReq
|
* @param searchReq
|
||||||
// * @return
|
* @return
|
||||||
// */
|
*/
|
||||||
// @Override
|
@Override
|
||||||
// public Page<Basic> findByMembers(MembersDto.SearchReq searchReq) {
|
public Page<Basic> findByMembers(MembersDto.SearchReq searchReq) {
|
||||||
// Pageable pageable = searchReq.toPageable();
|
Pageable pageable = searchReq.toPageable();
|
||||||
// BooleanBuilder builder = new BooleanBuilder();
|
BooleanBuilder builder = new BooleanBuilder();
|
||||||
// BooleanBuilder leftBuilder = new BooleanBuilder();
|
|
||||||
//
|
// 검색어
|
||||||
// if (StringUtils.isNotBlank(searchReq.getField())) {
|
if (StringUtils.isNotBlank(searchReq.getKeyword())) {
|
||||||
// switch (searchReq.getField()) {
|
String contains = "%" + searchReq.getKeyword() + "%";
|
||||||
// case "name" ->
|
|
||||||
// builder.and(memberEntity.name.containsIgnoreCase(searchReq.getKeyword().trim()));
|
builder.and(
|
||||||
// }
|
memberEntity
|
||||||
// }
|
.name
|
||||||
//
|
.likeIgnoreCase(contains)
|
||||||
// List<String> roles = new ArrayList<>();
|
.or(memberEntity.userId.likeIgnoreCase(contains))
|
||||||
// // 라벨러
|
.or(memberEntity.employeeNo.likeIgnoreCase(contains)));
|
||||||
// if (searchReq.isLabeler()) {
|
}
|
||||||
// roles.add(RoleType.ROLE_LABELER.getId());
|
|
||||||
// }
|
// 권한
|
||||||
//
|
if (StringUtils.isNotBlank(searchReq.getUserRole())) {
|
||||||
// // 시스템 전체 관리자
|
builder.and(memberEntity.userRole.eq(searchReq.getUserRole()));
|
||||||
// if (searchReq.isAdmin()) {
|
}
|
||||||
// roles.add(RoleType.ROLE_ADMIN.getId());
|
|
||||||
// }
|
List<MembersDto.Basic> content =
|
||||||
//
|
queryFactory
|
||||||
// // 검수자
|
.select(
|
||||||
// if (searchReq.isReviewer()) {
|
Projections.constructor(
|
||||||
// roles.add(RoleType.ROLE_REVIEWER.getId());
|
MembersDto.Basic.class,
|
||||||
// }
|
memberEntity.id,
|
||||||
//
|
memberEntity.uuid,
|
||||||
// // 역할 in 조건 추가
|
memberEntity.userRole,
|
||||||
// if (!roles.isEmpty()) {
|
memberEntity.name,
|
||||||
// leftBuilder.and(memberRoleEntity.id.roleName.in(roles));
|
memberEntity.userId,
|
||||||
// }
|
memberEntity.employeeNo,
|
||||||
//
|
memberEntity.tempPassword,
|
||||||
// List<MembersDto.Basic> content =
|
memberEntity.status,
|
||||||
// queryFactory
|
memberEntity.createdDttm,
|
||||||
// .select(
|
memberEntity.updatedDttm,
|
||||||
// Projections.constructor(
|
memberEntity.firstLoginDttm,
|
||||||
// MembersDto.Basic.class,
|
memberEntity.lastLoginDttm))
|
||||||
// memberEntity.id,
|
.from(memberEntity)
|
||||||
// memberEntity.uuid,
|
.where(builder)
|
||||||
// memberEntity.employeeNo,
|
.offset(pageable.getOffset())
|
||||||
// memberEntity.name,
|
.limit(pageable.getPageSize())
|
||||||
// null,
|
.orderBy(memberEntity.createdDttm.desc())
|
||||||
// memberEntity.status,
|
.fetch();
|
||||||
// memberRoleEntity.id.roleName,
|
|
||||||
// memberEntity.createdDttm,
|
long total = queryFactory.select(memberEntity).from(memberEntity).fetchCount();
|
||||||
// memberEntity.updatedDttm))
|
|
||||||
// .from(memberEntity)
|
return new PageImpl<>(content, pageable, total);
|
||||||
// .leftJoin(memberRoleEntity)
|
}
|
||||||
// .on(memberRoleEntity.memberUuid.uuid.eq(memberEntity.uuid).and(leftBuilder))
|
|
||||||
// .where(builder)
|
/**
|
||||||
// .offset(pageable.getOffset())
|
* 사용자 ID 조회 UUID
|
||||||
// .limit(pageable.getPageSize())
|
*
|
||||||
// .orderBy(memberEntity.createdDttm.desc())
|
* @param uuid
|
||||||
// .fetch();
|
* @return
|
||||||
//
|
*/
|
||||||
// long total =
|
|
||||||
// queryFactory
|
|
||||||
// .select(memberEntity)
|
|
||||||
// .from(memberEntity)
|
|
||||||
// .leftJoin(memberRoleEntity)
|
|
||||||
// .on(memberRoleEntity.memberUuid.uuid.eq(memberEntity.uuid).and(leftBuilder))
|
|
||||||
// .fetchCount();
|
|
||||||
//
|
|
||||||
// return new PageImpl<>(content, pageable, total);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<MemberEntity> findByUUID(UUID uuid) {
|
public Optional<MemberEntity> findByUUID(UUID uuid) {
|
||||||
return Optional.ofNullable(
|
return Optional.ofNullable(
|
||||||
|
|||||||
Reference in New Issue
Block a user