Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
9934b7d
MOSU-270 refactor: 전화번호로 사용자 검증
wlgns12370 Aug 10, 2025
2521b11
MOSU-270 refactor: 성별 응답 Custom
wlgns12370 Aug 10, 2025
0a9d3ab
MOSU-273 feat: 등록된 전화번호 확인 구현
wlgns12370 Aug 10, 2025
6527121
MOSU-273 feat: 등록된 전화번호 확인 Whitelist 추가
wlgns12370 Aug 10, 2025
5de298d
MOSU-273 fix: DataIntegrityViolationException 처리
wlgns12370 Aug 10, 2025
56d4ffe
MOSU-273 fix: 휴대폰 번호 확인 엔드 포인트 변경
wlgns12370 Aug 10, 2025
445393a
Merge remote-tracking branch 'origin/develop' into feature/mosu-273
wlgns12370 Aug 10, 2025
c0fa37c
Merge pull request #275 from mosu-dev/refactor/mosu-270
wlgns12370 Aug 10, 2025
348ff8a
Merge remote-tracking branch 'origin/develop' into feature/mosu-273
wlgns12370 Aug 10, 2025
e6b5d58
MOSU-273 fix: 검증 어노테이션 import 추가
wlgns12370 Aug 10, 2025
0373d2b
MOSU-273 refactor: API 연동을 위한 임시 secure false
wlgns12370 Aug 10, 2025
4cba953
Merge pull request #274 from mosu-dev/feature/mosu-273
wlgns12370 Aug 10, 2025
95e6ff8
MOSU refactor: 쿠키 domain dev.mosuedu.com 으로 변경
wlgns12370 Aug 10, 2025
a63a58b
MOSU refactor: 쿠키 domain .mosuedu.com 으로 변경
wlgns12370 Aug 10, 2025
91ddf3a
feat: add method to calculate refund amount in RefundService
polyglot-k Aug 10, 2025
f783688
feat: add endpoint to retrieve refund amount in RefundController
polyglot-k Aug 10, 2025
13a6ca9
feat: add RefundAmountResponse DTO for refund amount response
polyglot-k Aug 10, 2025
96f7f5f
MOSU refactor: 쿠키 prod 설정
wlgns12370 Aug 10, 2025
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
Expand Up @@ -18,7 +18,8 @@ public class SignUpAccountStepProcessor implements StepProcessor<UserJpaEntity,
@Transactional
@Override
public UserJpaEntity process(UserJpaEntity user) {
if (userRepository.existsByLoginId(user.getLoginId())) {
if (userRepository.existsByPhoneNumber(user.getPhoneNumber())
|| userRepository.existsByLoginId(user.getLoginId())) {
throw new CustomRuntimeException(ErrorCode.USER_ALREADY_EXISTS);
}
return userRepository.save(user);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class OAuthUserPersistenceProcessor implements StepProcessor<OAuthUserInf
@Override
@Transactional
public UserJpaEntity process(final OAuthUserInfo info) {
return userRepository.findByLoginId(info.email())
return userRepository.findByPhoneNumber(info.phoneNumber())

Choose a reason for hiding this comment

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

high

Changing the user lookup from loginId (email) to phoneNumber introduces a potential issue in the orElseGet block (lines 31-45). If no user is found by phone number, the code proceeds to create a new user using info.email() as the loginId. However, it doesn't check if a user with that loginId already exists. This can lead to an unhandled DataIntegrityViolationException if the email is already in use by another account (e.g., one created via standard signup). This should be handled gracefully to avoid a 500 error.

.map(existingUser -> {
existingUser.updateOAuthUser(
info.gender(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
import life.mosu.mosuserver.global.exception.ErrorCode;
import life.mosu.mosuserver.infra.toss.dto.CancelTossPaymentResponse;
import life.mosu.mosuserver.presentation.refund.dto.MergedRefundRequest;
import life.mosu.mosuserver.presentation.refund.dto.RefundAmountResponse;
import life.mosu.mosuserver.presentation.refund.dto.RefundRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;


Expand All @@ -28,6 +30,16 @@ public class RefundService {
private final TossRefundProcessor tossRefundProcessor;
private final PaymentJpaRepository paymentJpaRepository;

@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public RefundAmountResponse getRefundAmount(String paymentKey, Long examApplicationId) {
PaymentWithLunchProjection targetPayment = findPaymentOrThrow(paymentKey,
examApplicationId);

int totalQuantity = getTotalPaymentCount(paymentKey);
int refundAmount = calculateRefundAmount(totalQuantity, targetPayment.lunchChecked());
return RefundAmountResponse.of(refundAmount);
}

@Transactional
public void doProcess(Long userId, MergedRefundRequest request) {
RefundRequest details = request.details();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,8 @@ public UserInfoResponse getUserInfo(Long userId) {

return UserInfoResponse.from(user);
}

public Boolean isPhoneNumberAvailable(String phoneNumber) {
return !userJpaRepository.existsByPhoneNumber(phoneNumber);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ public interface UserJpaRepository extends JpaRepository<UserJpaEntity, Long> {

boolean existsByLoginId(String loginId);


Optional<UserJpaEntity> findByNameAndPhoneNumber(String name, String phoneNumber);

Optional<UserJpaEntity> findByPhoneNumber(String phoneNumber);

boolean existsByPhoneNumber(String phoneNumber);
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ public enum Whitelist {
FAQ("/api/v1/faq", WhitelistMethod.GET),
NOTICE("/api/v1/notice", WhitelistMethod.GET),
USER_ID_CHECK("/api/v1/user/check-id", WhitelistMethod.GET),
USER_PHONE_NUMBER_CHECK("/api/v1/user/check-phone-number", WhitelistMethod.GET),

CUSTOMER_KEY_CHECK("/api/v1/user/customer-key", WhitelistMethod.GET),

Choose a reason for hiding this comment

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

critical

The endpoint /api/v1/user/customer-key has been added to the whitelist, making it publicly accessible without authentication. However, the corresponding controller method UserController.getCustomerKey is protected with @PreAuthorize("isAuthenticated() and hasRole('USER')"), which requires an authenticated user. This creates a security vulnerability. If the authentication filter is bypassed, the security context will be empty, and the @PreAuthorize check will fail, likely causing an exception. This line should be removed if the endpoint is intended to be protected.

EXAM("/api/v1/exam", WhitelistMethod.GET),
EXAM_AREAS("/api/v1/exam/areas", WhitelistMethod.GET),
EXAM_ALL("/api/v1/exam/all", WhitelistMethod.GET),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@
import life.mosu.mosuserver.global.annotation.UserId;
import life.mosu.mosuserver.global.util.ApiResponseWrapper;
import life.mosu.mosuserver.presentation.refund.dto.MergedRefundRequest;
import life.mosu.mosuserver.presentation.refund.dto.RefundAmountResponse;
import life.mosu.mosuserver.presentation.refund.dto.RefundRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
Expand All @@ -24,6 +27,15 @@ public class RefundController {

private final RefundService refundService;

@GetMapping()
ResponseEntity<ApiResponseWrapper<RefundAmountResponse>> getRefundAmount(
@RequestParam(required = true) String paymentKey,
@RequestParam(required = true) Long examApplicationId) {
var response = refundService.getRefundAmount(paymentKey, examApplicationId);
return ResponseEntity.ok(
ApiResponseWrapper.success(HttpStatus.OK, "환불 금액 조회 성공", response));
}
Comment on lines +30 to +37

Choose a reason for hiding this comment

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

high

The new getRefundAmount endpoint is not protected by any authentication or authorization mechanism. This means anyone who knows a valid paymentKey and examApplicationId can query the refund amount. This could lead to information leakage. It is recommended to secure this endpoint, for example by adding @PreAuthorize("isAuthenticated()") and verifying that the authenticated user is authorized to view this information.


@PostMapping("/{paymentKey}")
@PreAuthorize("isAuthenticated() and hasRole('USER')")
ResponseEntity<ApiResponseWrapper<Void>> process(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package life.mosu.mosuserver.presentation.refund.dto;

public record RefundAmountResponse(
int amount
) {

public static RefundAmountResponse of(int amount) {
return new RefundAmountResponse(amount);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import life.mosu.mosuserver.application.user.UserService;
import life.mosu.mosuserver.global.annotation.LoginIdPattern;
import life.mosu.mosuserver.global.annotation.PhoneNumberPattern;
import life.mosu.mosuserver.global.annotation.UserId;
import life.mosu.mosuserver.global.util.ApiResponseWrapper;
import life.mosu.mosuserver.presentation.user.dto.request.IsLoginIdAvailableResponse;
import life.mosu.mosuserver.presentation.user.dto.response.CustomerKeyResponse;
import life.mosu.mosuserver.presentation.user.dto.response.IsLoginIdAvailableResponse;
import life.mosu.mosuserver.presentation.user.dto.response.IsPhoneNumberAvailableResponse;
import life.mosu.mosuserver.presentation.user.dto.response.UserInfoResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
Expand Down Expand Up @@ -58,4 +60,14 @@ public ResponseEntity<ApiResponseWrapper<IsLoginIdAvailableResponse>> isLoginIdA
IsLoginIdAvailableResponse.from(isLoginIdAvailable)));
}

@GetMapping("/check-phone-number")
public ResponseEntity<ApiResponseWrapper<IsPhoneNumberAvailableResponse>> isPhoneNumberAvailable(
@PhoneNumberPattern @RequestParam String phoneNumber
) {
Boolean isPhoneNumberAvailable = userService.isPhoneNumberAvailable(phoneNumber);

return ResponseEntity.ok(
ApiResponseWrapper.success(HttpStatus.OK, "User PhoneNumber 등록 가능 여부 조회 성공",
IsPhoneNumberAvailableResponse.from(isPhoneNumberAvailable)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import life.mosu.mosuserver.global.annotation.LoginIdPattern;
import life.mosu.mosuserver.global.annotation.UserId;
import life.mosu.mosuserver.global.util.ApiResponseWrapper;
import life.mosu.mosuserver.presentation.user.dto.request.IsLoginIdAvailableResponse;
import life.mosu.mosuserver.presentation.user.dto.response.CustomerKeyResponse;
import life.mosu.mosuserver.presentation.user.dto.response.IsLoginIdAvailableResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestParam;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package life.mosu.mosuserver.presentation.user.dto.request;
package life.mosu.mosuserver.presentation.user.dto.response;

public record IsLoginIdAvailableResponse(
Boolean isLoginIdAvailable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package life.mosu.mosuserver.presentation.user.dto.response;

public record IsPhoneNumberAvailableResponse(
Boolean isPhoneNumberAvailable
) {

public static IsPhoneNumberAvailableResponse from(Boolean isPhoneNumberAvailable) {
return new IsPhoneNumberAvailableResponse(isPhoneNumberAvailable);
}
}