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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package leets.weeth.domain.user.application.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
Expand Down Expand Up @@ -27,7 +28,10 @@ public record SignUp(
}

public record Register(
@NotNull Long kakaoId,
@Schema(description = "kakao로 회원가입 하는 경우")
Long kakaoId,
@Schema(description = "애플로 회원가입 하는 경우 - Apple OAuth authCode")
String appleAuthCode,
@NotBlank String name,
@NotBlank String studentId,
@NotBlank String email,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class UserResponseDto {
public record SocialLoginResponse(
Long id,
Long kakaoId,
String appleIdToken,
LoginStatus status,
String accessToken,
String refreshToken
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,15 @@ public interface UserMapper {
@Mapping(target = "status", expression = "java(LoginStatus.LOGIN)"),
@Mapping(target = "id", source = "user.id"),
@Mapping(target = "kakaoId", source = "user.kakaoId"),
@Mapping(target = "appleIdToken", expression = "java(null)")
})
SocialLoginResponse toLoginResponse(User user, JwtDto dto);

@Mappings({
@Mapping(target = "status", expression = "java(LoginStatus.INTEGRATE)"),
@Mapping(target = "appleIdToken", expression = "java(null)"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

appleIdToken는 왜 null값이 들어가나요? kakaoId는 null값이 아닌거로 이해했어요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 해당 메서드가 카카오 연동 과정에서만 쓰이고 있어요! 애플은 toAppleIntegrateResponse로 별도로 채워서 보여주고 있습니당

@Mapping(target = "accessToken", expression = "java(null)"),
@Mapping(target = "refreshToken", expression = "java(null)")
})
SocialLoginResponse toIntegrateResponse(Long kakaoId);

Expand All @@ -66,6 +70,24 @@ public interface UserMapper {
@Mapping(target = "cardinals", expression = "java( toCardinalNumbers(userCardinals) )")
UserResponseDto.UserInfo toUserInfoDto(User user, List<UserCardinal> userCardinals);

@Mappings({
@Mapping(target = "status", expression = "java(LoginStatus.LOGIN)"),
@Mapping(target = "id", source = "user.id"),
@Mapping(target = "appleIdToken", expression = "java(null)"),
@Mapping(target = "kakaoId", expression = "java(null)")
})
SocialLoginResponse toAppleLoginResponse(User user, JwtDto dto);

@Mappings({
@Mapping(target = "status", expression = "java(LoginStatus.INTEGRATE)"),
@Mapping(target = "id", expression = "java(null)"),
@Mapping(target = "appleIdToken", source = "appleIdToken"),
@Mapping(target = "kakaoId", expression = "java(null)"),
@Mapping(target = "accessToken", expression = "java(null)"),
@Mapping(target = "refreshToken", expression = "java(null)")
})
SocialLoginResponse toAppleIntegrateResponse(String appleIdToken);

default String toString(Department department) {
return department.getValue();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,8 @@ public interface UserUseCase {

List<SummaryResponse> searchUser(String keyword);

SocialLoginResponse appleLogin(Login dto);

void appleRegister(Register dto);

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,24 @@
import leets.weeth.domain.user.domain.entity.User;
import leets.weeth.domain.user.domain.entity.UserCardinal;
import leets.weeth.domain.user.domain.service.*;
import leets.weeth.global.auth.apple.dto.AppleTokenResponse;
import leets.weeth.global.auth.apple.dto.AppleUserInfo;
import leets.weeth.global.auth.jwt.application.dto.JwtDto;
import leets.weeth.global.auth.jwt.application.usecase.JwtManageUseCase;
import leets.weeth.global.auth.kakao.KakaoAuthService;
import leets.weeth.global.auth.kakao.dto.KakaoTokenResponse;
import leets.weeth.global.auth.kakao.dto.KakaoUserInfoResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.*;
import java.util.stream.Collectors;

import static leets.weeth.domain.user.application.dto.request.UserRequestDto.*;
Expand All @@ -44,13 +44,15 @@ public class UserUseCaseImpl implements UserUseCase {
private final UserGetService userGetService;
private final UserUpdateService userUpdateService;
private final KakaoAuthService kakaoAuthService;
private final leets.weeth.global.auth.apple.AppleAuthService appleAuthService;
private final CardinalGetService cardinalGetService;
private final UserCardinalSaveService userCardinalSaveService;
private final UserCardinalGetService userCardinalGetService;

private final UserMapper mapper;
private final CardinalMapper cardinalMapper;
private final PasswordEncoder passwordEncoder;
private final Environment environment;

@Override
@Transactional(readOnly = true)
Expand Down Expand Up @@ -238,4 +240,75 @@ private UserCardinalDto getUserCardinalDto(Long userId) {

return cardinalMapper.toUserCardinalDto(user, userCardinals);
}

@Override
@Transactional(readOnly = true)
public SocialLoginResponse appleLogin(Login dto) {
// Apple Token 요청 및 유저 정보 요청
AppleTokenResponse tokenResponse = appleAuthService.getAppleToken(dto.authCode());
AppleUserInfo userInfo = appleAuthService.verifyAndDecodeIdToken(tokenResponse.id_token());

String appleIdToken = tokenResponse.id_token();
String appleId = userInfo.appleId();

Optional<User> optionalUser = userGetService.findByAppleId(appleId);

//todo: 추후 애플 로그인 연동을 위해 appleIdToken을 반환
// 애플 로그인 연동 API 요청시 appleIdToken을 함께 넣어주면 그때 디코딩해서 appleId를 추출
if (optionalUser.isEmpty()) {
return mapper.toAppleIntegrateResponse(appleIdToken);
}

User user = optionalUser.get();
if (user.isInactive()) {
throw new UserInActiveException();
}

JwtDto token = jwtManageUseCase.create(user.getId(), user.getEmail(), user.getRole());
return mapper.toAppleLoginResponse(user, token);
}

@Override
@Transactional
public void appleRegister(Register dto) {
validate(dto);

// Apple authCode로 토큰 교환 후 ID Token 검증 및 사용자 정보 추출
AppleTokenResponse tokenResponse = appleAuthService.getAppleToken(dto.appleAuthCode());
AppleUserInfo appleUserInfo = appleAuthService.verifyAndDecodeIdToken(tokenResponse.id_token());

Cardinal cardinal = cardinalGetService.findByUserSide(dto.cardinal());

User user = mapper.from(dto);
// Apple ID 설정
user.addAppleId(appleUserInfo.appleId());

UserCardinal userCardinal = new UserCardinal(user, cardinal);

userSaveService.save(user);
userCardinalSaveService.save(userCardinal);

// dev 환경에서만 바로 ACTIVE 상태로 설정
if (isDevEnvironment()) {
log.info("dev 환경 감지: 사용자 자동 승인 처리 (userId: {})", user.getId());
user.accept();
}
}

/**
* 현재 환경이 dev 프로파일인지 확인
* @return dev 프로파일이 활성화되어 있으면 true
*/
private boolean isDevEnvironment() {
String[] activeProfiles = environment.getActiveProfiles();
for (String profile : activeProfiles) {
if ("dev".equals(profile)) {
return true;
}
if ("local".equals(profile)) {
return true;
}
}
return false;
}
}
29 changes: 14 additions & 15 deletions src/main/java/leets/weeth/domain/user/domain/entity/User.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,6 @@
package leets.weeth.domain.user.domain.entity;

import static leets.weeth.domain.user.application.dto.request.UserRequestDto.Update;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Table;
import java.util.ArrayList;
import java.util.List;
import jakarta.persistence.*;
import leets.weeth.domain.attendance.domain.entity.Attendance;
import leets.weeth.domain.board.domain.entity.enums.Part;
import leets.weeth.domain.user.domain.entity.enums.Department;
Expand All @@ -29,6 +15,11 @@
import lombok.experimental.SuperBuilder;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.ArrayList;
import java.util.List;

import static leets.weeth.domain.user.application.dto.request.UserRequestDto.Update;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
Expand All @@ -42,8 +33,12 @@ public class User extends BaseEntity {
@Column(name = "user_id")
private Long id;

@Column(unique = true)
private Long kakaoId;

@Column(unique = true)
private String appleId;

private String name;

private String email;
Expand Down Expand Up @@ -94,6 +89,10 @@ public void addKakaoId(long kakaoId) {
this.kakaoId = kakaoId;
}

public void addAppleId(String appleId) {
this.appleId = appleId;
}

public void leave() {
this.status = Status.LEFT;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public interface UserRepository extends JpaRepository<User, Long> {

Optional<User> findByKakaoId(long kakaoId);

Optional<User> findByAppleId(String appleId);

List<User>findAllByNameContainingAndStatus(String name, Status status);

boolean existsByEmail(String email);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ public Optional<User> find(long kakaoId){
return userRepository.findByKakaoId(kakaoId);
}

public Optional<User> findByAppleId(String appleId){
return userRepository.findByAppleId(appleId);
}

public List<User> search(String keyword) {
return userRepository.findAllByNameContainingAndStatus(keyword, Status.ACTIVE);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,20 @@ public CommonResponse<SocialLoginResponse> integrate(@RequestBody @Valid NormalL
return CommonResponse.createSuccess(SOCIAL_INTEGRATE_SUCCESS.getMessage(), userUseCase.integrate(dto));
}

@PostMapping("/apple/login")
@Operation(summary = "애플 소셜 로그인 API")
public CommonResponse<SocialLoginResponse> appleLogin(@RequestBody @Valid Login dto) {
SocialLoginResponse response = userUseCase.appleLogin(dto);
return CommonResponse.createSuccess(SOCIAL_LOGIN_SUCCESS.getMessage(), response);
}

@PostMapping("/apple/register")
@Operation(summary = "애플 소셜 회원가입 (dev 전용 - 바로 ACTIVE)")
public CommonResponse<Void> appleRegister(@RequestBody @Valid Register dto) {
userUseCase.appleRegister(dto);
return CommonResponse.createSuccess(USER_APPLY_SUCCESS.getMessage());
}
Comment on lines +78 to +83
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

개발 전용 자동 승인 로직의 프로덕션 사용을 검증하세요.

엔드포인트 설명에 "dev 전용 - 바로 ACTIVE"라고 명시되어 있으나, 코드 상에서는 환경에 따른 분기 처리가 보이지 않습니다. 앱 심사를 위한 의도적인 구현이라고 명시되어 있지만, 프로덕션 환경에서 이 엔드포인트가 노출되지 않도록 하거나 환경별로 승인 로직이 다르게 동작하는지 확인이 필요합니다.

다음 스크립트로 환경별 분기 처리 여부를 확인해주세요:


🏁 Script executed:

#!/bin/bash
# Description: Apple 회원가입 관련 환경별 분기 처리 확인

# Apple register 구현체에서 환경 변수나 프로파일 기반 분기 확인
rg -n "appleRegister" --type=java -A 20 | rg -i "profile|environment|active|dev|prod"

# 사용자 승인 관련 로직 확인
rg -n "setStatus|ACTIVE|PENDING" --type=java -g "*User*Impl.java" -C 5

Length of output: 1174


환경별 분기 처리를 더 자세히 확인하겠습니다.

추가 정보를 수집하겠습니다. 전체 메서드 구현과 클래스 레벨의 조건부 설정을 확인하겠습니다.

프로덕션 환경에서 자동 승인 로직이 보호되지 않습니다.

실행 결과 확인 결과, UserUseCaseImpl.javaappleRegister 메서드(293-294줄)에서 user.accept()가 환경별 조건 없이 무조건 실행되고 있습니다. 코드에 "dev 전용"이라는 주석이 있지만, 실제 환경 기반 분기 처리(예: @Profile, Environment 인젝션을 통한 조건 체크)가 없으므로 프로덕션 환경에서도 동일하게 자동 승인됩니다.

필수 조치:

  • appleRegister 메서드에 환경 확인 로직 추가 (예: Environment.getActiveProfiles()로 "dev" 프로파일만 검사)
  • 또는 @Profile("dev")appleRegister 메서드에 적용하여 dev 환경에서만 등록되도록 제한
  • 또는 설정 속성(@ConditionalOnProperty)을 활용하여 제어
🤖 Prompt for AI Agents
In src/main/java/leets/weeth/domain/user/presentation/UserController.java around
lines 78-83, the controller endpoint is labeled "dev 전용" but the actual
dangerous logic is in
src/main/java/leets/weeth/domain/user/application/UserUseCaseImpl.java around
lines 293-294 where user.accept() is invoked unconditionally; fix by restricting
automatic acceptance to dev only — either inject Spring Environment and check
Environment.getActiveProfiles() includes "dev" before calling user.accept(), or
annotate the method with @Profile("dev") (or use @ConditionalOnProperty and a
feature flag) so that user.accept() runs only when the dev profile/property is
active; ensure tests/beans still work by updating configuration and adding a
clear log/error path for non-dev flows.


@GetMapping("/email")
@Operation(summary = "이메일 중복 확인")
public CommonResponse<Boolean> checkEmail(@RequestParam String email) {
Expand Down
Loading