Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -138,14 +138,14 @@ tasks.named('jacocoTestCoverageVerification') {

violationRules {
// 1) 전체 커버리지: 후퇴 방지선(ratchet).
// 현재 라인 커버리지는 약 34% 다. 테스트를 늘릴 때마다 이 값을 함께 올린다.
// 현재 라인 커버리지는 약 37% 다. 테스트를 늘릴 때마다 이 값을 함께 올린다.
// (기존 0.01 은 사실상 게이트가 없는 것과 같아 통과해도 의미가 없었다.)
rule {
element = 'BUNDLE'
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.30
minimum = 0.35
}
}

Expand All @@ -157,6 +157,7 @@ tasks.named('jacocoTestCoverageVerification') {
// - JwtAuthenticationFilter: 누구로 인증되는지를 정한다.
// - RateLimitInterceptor: fail-open 이 깨지면 Redis 장애가 전면 장애가 된다.
// - EmailVerificationService: 인증번호 난수·시도 제한.
// - UserService: 가입·역할·계정 상태. 0.7% 였던 동안 여기서 P0 가 세 번 나왔다.
rule {
element = 'CLASS'
includes = [
Expand All @@ -166,7 +167,8 @@ tasks.named('jacocoTestCoverageVerification') {
'com.carecode.core.security.CurrentUserFacade',
'com.carecode.core.security.JwtAuthenticationFilter',
'com.carecode.core.RateLimitInterceptor',
'com.carecode.domain.user.service.EmailVerificationService'
'com.carecode.domain.user.service.EmailVerificationService',
'com.carecode.domain.user.service.UserService'
]
limit {
counter = 'LINE'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,11 @@ protected boolean shouldNotFilter(HttpServletRequest request) throws ServletExce
path.startsWith("/auth/login") ||
path.startsWith("/auth/register") ||
path.equals("/auth/refresh") ||
path.startsWith("/auth/kakao") ||
// /auth/kakao 전체가 아니다. 가입 완료(/auth/kakao/complete-registration)는
// 카카오 로그인이 발급한 토큰으로 호출되는데, 접두사로 통째로 건너뛰면 토큰이
// 해석되지 않아 항상 401 이었다. 카카오로 처음 들어온 사람은 가입을 끝낼 수 없었다.
path.equals("/auth/kakao/login") ||
path.equals("/auth/kakao/login-url") ||
path.startsWith("/oauth2") ||
path.startsWith("/login/oauth2") ||
path.equals("/kakao-callback.html");
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/com/carecode/core/security/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers("/auth/refresh").permitAll() // 토큰 갱신
.requestMatchers("/auth/kakao/login").permitAll() // 카카오 로그인
.requestMatchers("/auth/kakao/login-url").permitAll() // 카카오 로그인 URL 생성
.requestMatchers("/auth/kakao/complete-registration").permitAll() // 카카오 가입 완료
// 카카오 가입 완료는 로그인한 사용자가 자기 계정에 하는 동작이다(대상은 토큰의 이메일).
// permitAll 로 두면 JWT 필터 예외와 겹쳐 인증 없이 들어오고, 컨트롤러가 401 을 냈다.
.requestMatchers("/auth/kakao/complete-registration").authenticated()

// OAuth2 authorize/token (Spring Client beans).
.requestMatchers("/oauth2/**").permitAll()
Expand Down
32 changes: 25 additions & 7 deletions src/main/java/com/carecode/domain/user/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ public class UserService {
/** 자가 가입으로 만들어질 수 있는 유일한 역할. 그 이상은 관리자만 부여한다. */
private static final UserRole SELF_SIGNUP_ROLE = UserRole.PARENT;

/**
* 가입 과정에서 본인이 고를 수 있는 역할. 프런트 가입 화면은 PARENT 로 고정해 보낸다.
* ADMIN 은 관리자만 부여할 수 있다(PUT /api/admin/users/{id}/role).
*/
private static final java.util.Set<UserRole> SELF_SELECTABLE_ROLES =
java.util.EnumSet.of(UserRole.PARENT, UserRole.CAREGIVER);

private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final RestTemplate restTemplate;
Expand Down Expand Up @@ -203,17 +210,28 @@ public UserDto completeKakaoRegistration(String email, String name, String role)
throw new IllegalArgumentException("카카오 사용자가 아닙니다: " + email);
}

// 이미 가입 완료된 사용자인지 확인
if (user.getRegistrationCompleted()) {
// 이미 가입 완료된 사용자인지 확인 (Boolean 이라 null 이면 언박싱에서 터진다)
if (Boolean.TRUE.equals(user.getRegistrationCompleted())) {
throw new IllegalArgumentException("이미 가입 완료된 사용자입니다: " + email);
}

// 역할 유효성 검증

// 역할은 스스로 고를 수 있는 것만 받는다.
//
// 예전에는 요청 본문의 role 을 UserRole.valueOf 로 그대로 받았고, 오류 메시지가
// "가능한 역할: PARENT, CAREGIVER, ADMIN, GUEST" 라고 친절히 알려주기까지 했다.
// 즉 {"role":"ADMIN"} 으로 가입을 끝내면 그 자리에서 관리자가 됐다(이메일 가입의 #82 와 같다).
// 한동안 드러나지 않은 건 JWT 필터가 이 경로를 건너뛰어 흐름 자체가 401 로 막혀 있었기 때문이다.
//
// 금지 목록이 아니라 허용 목록이다. 나중에 특권 역할이 추가돼도 기본으로 막힌다.
UserRole userRole;
try {
userRole = UserRole.valueOf(role);
userRole = UserRole.valueOf(role.trim().toUpperCase());
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("유효하지 않은 역할입니다: " + role + ". 가능한 역할: PARENT, CAREGIVER, ADMIN, GUEST");
throw new IllegalArgumentException("유효하지 않은 역할입니다: " + role);
}
if (!SELF_SELECTABLE_ROLES.contains(userRole)) {
log.warn("카카오 가입 완료에서 스스로 고를 수 없는 역할 요청 - email={}, role={}", email, userRole);
throw new IllegalArgumentException("선택할 수 없는 역할입니다: " + role);
}

// 이름 및 역할 업데이트 및 가입 프로세스 완료 처리
Expand Down Expand Up @@ -566,7 +584,7 @@ public void reactivateUser(String userId) {
}

// 이미 활성화된 계정인지 확인
if (user.getIsActive() && user.getDeletedAt() == null) {
if (Boolean.TRUE.equals(user.getIsActive()) && user.getDeletedAt() == null) {
throw new IllegalArgumentException("이미 활성화된 계정입니다.");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ public TokenDto oAuthLoginOrRegister(String accessCode) {
user.setUpdatedAt(LocalDateTime.now());
userRepository.save(user);

if ("kakao".equals(user.getProvider()) && !user.getRegistrationCompleted()) {
// registrationCompleted 는 Boolean 이다. null 이면 !언박싱에서 NPE 로 로그인 전체가 500 이 된다.
if ("kakao".equals(user.getProvider()) && !Boolean.TRUE.equals(user.getRegistrationCompleted())) {
isNewUser = true;
}
} else {
Expand Down
Loading
Loading