diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index 085343dfd..cbcd29627 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -11,12 +11,10 @@ import com.example.solidconnection.auth.dto.oauth.OAuthResponse; import com.example.solidconnection.auth.dto.oauth.OAuthSignInResponse; import com.example.solidconnection.auth.service.AuthService; -import com.example.solidconnection.auth.service.CommonSignUpTokenProvider; import com.example.solidconnection.auth.service.EmailSignInService; -import com.example.solidconnection.auth.service.EmailSignUpService; import com.example.solidconnection.auth.service.EmailSignUpTokenProvider; +import com.example.solidconnection.auth.service.SignUpService; import com.example.solidconnection.auth.service.oauth.OAuthService; -import com.example.solidconnection.auth.service.oauth.OAuthSignUpService; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.common.exception.ErrorCode; import com.example.solidconnection.common.resolver.AuthorizedUser; @@ -38,12 +36,10 @@ public class AuthController { private final AuthService authService; - private final OAuthSignUpService oAuthSignUpService; private final OAuthService oAuthService; + private final SignUpService signUpService; private final EmailSignInService emailSignInService; - private final EmailSignUpService emailSignUpService; private final EmailSignUpTokenProvider emailSignUpTokenProvider; - private final CommonSignUpTokenProvider commonSignUpTokenProvider; private final RefreshTokenCookieManager refreshTokenCookieManager; @PostMapping("/apple") @@ -85,8 +81,7 @@ public ResponseEntity signInWithEmail( public ResponseEntity signUpWithEmail( @Valid @RequestBody EmailSignUpTokenRequest signUpRequest ) { - emailSignUpService.validateUniqueEmail(signUpRequest.email()); - String signUpToken = emailSignUpTokenProvider.generateAndSaveSignUpToken(signUpRequest); + String signUpToken = emailSignUpTokenProvider.issueEmailSignUpToken(signUpRequest); return ResponseEntity.ok(new EmailSignUpTokenResponse(signUpToken)); } @@ -94,12 +89,7 @@ public ResponseEntity signUpWithEmail( public ResponseEntity signUp( @Valid @RequestBody SignUpRequest signUpRequest ) { - AuthType authType = commonSignUpTokenProvider.parseAuthType(signUpRequest.signUpToken()); - if (AuthType.isEmail(authType)) { - SignInResponse signInResponse = emailSignUpService.signUp(signUpRequest); - return ResponseEntity.ok(signInResponse); - } - SignInResponse signInResponse = oAuthSignUpService.signUp(signUpRequest); + SignInResponse signInResponse = signUpService.signUp(signUpRequest); return ResponseEntity.ok(signInResponse); } diff --git a/src/main/java/com/example/solidconnection/auth/service/CommonSignUpTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/CommonSignUpTokenProvider.java deleted file mode 100644 index c6930315b..000000000 --- a/src/main/java/com/example/solidconnection/auth/service/CommonSignUpTokenProvider.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.example.solidconnection.auth.service; - -import static com.example.solidconnection.auth.service.EmailSignUpTokenProvider.AUTH_TYPE_CLAIM_KEY; -import static com.example.solidconnection.common.exception.ErrorCode.SIGN_UP_TOKEN_INVALID; - -import com.example.solidconnection.common.exception.CustomException; -import com.example.solidconnection.siteuser.domain.AuthType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class CommonSignUpTokenProvider { - - private final TokenProvider tokenProvider; - - public AuthType parseAuthType(String signUpToken) { - try { - String authTypeStr = tokenProvider.parseClaims(signUpToken).get(AUTH_TYPE_CLAIM_KEY, String.class); - return AuthType.valueOf(authTypeStr); - } catch (Exception e) { - throw new CustomException(SIGN_UP_TOKEN_INVALID); - } - } -} diff --git a/src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java b/src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java index d7ee365d8..4dac56586 100644 --- a/src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java +++ b/src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java @@ -1,6 +1,6 @@ package com.example.solidconnection.auth.service; -import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; +import static com.example.solidconnection.common.exception.ErrorCode.SIGN_IN_FAILED; import com.example.solidconnection.auth.dto.EmailSignInRequest; import com.example.solidconnection.auth.dto.SignInResponse; @@ -8,14 +8,11 @@ import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -/* - * 보안을 위해 이메일과 비밀번호 중 무엇이 틀렸는지 구체적으로 응답하지 않는다. - * */ @Service @RequiredArgsConstructor public class EmailSignInService { @@ -24,19 +21,21 @@ public class EmailSignInService { private final SiteUserRepository siteUserRepository; private final PasswordEncoder passwordEncoder; + @Transactional(readOnly = true) public SignInResponse signIn(EmailSignInRequest signInRequest) { - Optional optionalSiteUser = siteUserRepository.findByEmailAndAuthType(signInRequest.email(), AuthType.EMAIL); - if (optionalSiteUser.isPresent()) { - SiteUser siteUser = optionalSiteUser.get(); - validatePassword(signInRequest.password(), siteUser.getPassword()); - return signInService.signIn(siteUser); - } - throw new CustomException(USER_NOT_FOUND, "이메일과 비밀번호를 확인해주세요."); + SiteUser siteUser = getEmailMatchingUserOrThrow(signInRequest.email()); + validatePassword(signInRequest.password(), siteUser.getPassword()); + return signInService.signIn(siteUser); + } + + private SiteUser getEmailMatchingUserOrThrow(String email) { + return siteUserRepository.findByEmailAndAuthType(email, AuthType.EMAIL) + .orElseThrow(() -> new CustomException(SIGN_IN_FAILED)); } private void validatePassword(String rawPassword, String encodedPassword) { if (!passwordEncoder.matches(rawPassword, encodedPassword)) { - throw new CustomException(USER_NOT_FOUND, "이메일과 비밀번호를 확인해주세요."); + throw new CustomException(SIGN_IN_FAILED); } } } diff --git a/src/main/java/com/example/solidconnection/auth/service/EmailSignUpService.java b/src/main/java/com/example/solidconnection/auth/service/EmailSignUpService.java deleted file mode 100644 index a3436cf5d..000000000 --- a/src/main/java/com/example/solidconnection/auth/service/EmailSignUpService.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.example.solidconnection.auth.service; - -import static com.example.solidconnection.common.exception.ErrorCode.USER_ALREADY_EXISTED; - -import com.example.solidconnection.auth.dto.SignUpRequest; -import com.example.solidconnection.common.exception.CustomException; -import com.example.solidconnection.location.country.repository.CountryRepository; -import com.example.solidconnection.location.country.repository.InterestedCountryRepository; -import com.example.solidconnection.location.region.repository.InterestedRegionRepository; -import com.example.solidconnection.location.region.repository.RegionRepository; -import com.example.solidconnection.siteuser.domain.AuthType; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import org.springframework.stereotype.Service; - -@Service -public class EmailSignUpService extends SignUpService { - - private final EmailSignUpTokenProvider emailSignUpTokenProvider; - - public EmailSignUpService(SignInService signInService, SiteUserRepository siteUserRepository, - RegionRepository regionRepository, InterestedRegionRepository interestedRegionRepository, - CountryRepository countryRepository, InterestedCountryRepository interestedCountryRepository, - EmailSignUpTokenProvider emailSignUpTokenProvider) { - super(signInService, siteUserRepository, regionRepository, interestedRegionRepository, countryRepository, interestedCountryRepository); - this.emailSignUpTokenProvider = emailSignUpTokenProvider; - } - - public void validateUniqueEmail(String email) { - if (siteUserRepository.existsByEmailAndAuthType(email, AuthType.EMAIL)) { - throw new CustomException(USER_ALREADY_EXISTED); - } - } - - @Override - protected void validateSignUpToken(SignUpRequest signUpRequest) { - emailSignUpTokenProvider.validateSignUpToken(signUpRequest.signUpToken()); - } - - @Override - protected void validateUserNotDuplicated(SignUpRequest signUpRequest) { - String email = emailSignUpTokenProvider.parseEmail(signUpRequest.signUpToken()); - if (siteUserRepository.existsByEmailAndAuthType(email, AuthType.EMAIL)) { - throw new CustomException(USER_ALREADY_EXISTED); - } - } - - @Override - protected SiteUser createSiteUser(SignUpRequest signUpRequest) { - String email = emailSignUpTokenProvider.parseEmail(signUpRequest.signUpToken()); - String encodedPassword = emailSignUpTokenProvider.parseEncodedPassword(signUpRequest.signUpToken()); - return signUpRequest.toEmailSiteUser(email, encodedPassword); - } -} diff --git a/src/main/java/com/example/solidconnection/auth/service/EmailSignUpTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/EmailSignUpTokenProvider.java index 238c7e517..a3e2e5dc9 100644 --- a/src/main/java/com/example/solidconnection/auth/service/EmailSignUpTokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/EmailSignUpTokenProvider.java @@ -1,88 +1,32 @@ package com.example.solidconnection.auth.service; -import static com.example.solidconnection.common.exception.ErrorCode.SIGN_UP_TOKEN_INVALID; -import static com.example.solidconnection.common.exception.ErrorCode.SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER; - -import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.auth.dto.EmailSignUpTokenRequest; -import com.example.solidconnection.auth.token.config.JwtProperties; import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.common.exception.ErrorCode; import com.example.solidconnection.siteuser.domain.AuthType; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; @Component @RequiredArgsConstructor public class EmailSignUpTokenProvider { - static final String PASSWORD_CLAIM_KEY = "password"; - static final String AUTH_TYPE_CLAIM_KEY = "authType"; - - private final PasswordEncoder passwordEncoder; - private final JwtProperties jwtProperties; - private final RedisTemplate redisTemplate; - private final TokenProvider tokenProvider; + private final SignUpTokenProvider signUpTokenProvider; + private final SiteUserRepository siteUserRepository; + private final PasswordTemporaryStorage passwordTemporaryStorage; - public String generateAndSaveSignUpToken(EmailSignUpTokenRequest request) { + @Transactional(readOnly = true) + public String issueEmailSignUpToken(EmailSignUpTokenRequest request) { String email = request.email(); String password = request.password(); - String encodedPassword = passwordEncoder.encode(password); - Map emailSignUpClaims = new HashMap<>(Map.of( - PASSWORD_CLAIM_KEY, encodedPassword, - AUTH_TYPE_CLAIM_KEY, AuthType.EMAIL - )); - Claims claims = Jwts.claims(emailSignUpClaims).setSubject(email); - Date now = new Date(); - Date expiredDate = new Date(now.getTime() + TokenType.SIGN_UP.getExpireTime()); - - String signUpToken = Jwts.builder() - .setClaims(claims) - .setIssuedAt(now) - .setExpiration(expiredDate) - .signWith(SignatureAlgorithm.HS512, jwtProperties.secret()) - .compact(); - return tokenProvider.saveToken(signUpToken, TokenType.SIGN_UP); - } - - public void validateSignUpToken(String token) { - validateFormatAndExpiration(token); - String email = parseEmail(token); - validateIssuedByServer(email); - } - private void validateFormatAndExpiration(String token) { - try { - Claims claims = tokenProvider.parseClaims(token); - Objects.requireNonNull(claims.getSubject()); - String encodedPassword = claims.get(PASSWORD_CLAIM_KEY, String.class); - Objects.requireNonNull(encodedPassword); - } catch (Exception e) { - throw new CustomException(SIGN_UP_TOKEN_INVALID); + if (siteUserRepository.existsByEmailAndAuthType(email, AuthType.EMAIL)) { + throw new CustomException(ErrorCode.USER_ALREADY_EXISTED); } - } - - private void validateIssuedByServer(String email) { - String key = TokenType.SIGN_UP.addPrefix(email); - if (redisTemplate.opsForValue().get(key) == null) { - throw new CustomException(SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER); - } - } - - public String parseEmail(String token) { - return tokenProvider.parseSubject(token); - } - public String parseEncodedPassword(String token) { - Claims claims = tokenProvider.parseClaims(token); - return claims.get(PASSWORD_CLAIM_KEY, String.class); + passwordTemporaryStorage.save(email, password); + return signUpTokenProvider.generateAndSaveSignUpToken(email, AuthType.EMAIL); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/PasswordTemporaryStorage.java b/src/main/java/com/example/solidconnection/auth/service/PasswordTemporaryStorage.java new file mode 100644 index 000000000..adcb8bf68 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/PasswordTemporaryStorage.java @@ -0,0 +1,46 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.domain.TokenType; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class PasswordTemporaryStorage { + + private static final String KEY_PREFIX = "password:"; + + private final RedisTemplate redisTemplate; + private final PasswordEncoder passwordEncoder; + + public void save(String email, String rawPassword) { + String encodedPassword = passwordEncoder.encode(rawPassword); + redisTemplate.opsForValue().set( + convertToKey(email), + encodedPassword, + TokenType.SIGN_UP.getExpireTime(), + TimeUnit.MILLISECONDS + ); + } + + public Optional findByEmail(String email) { + String encodedPassword = redisTemplate.opsForValue().get(convertToKey(email)); + if (encodedPassword == null) { + return Optional.empty(); + } + return Optional.of(encodedPassword); + } + + public void deleteByEmail(String email) { + String key = convertToKey(email); + redisTemplate.delete(key); + } + + private String convertToKey(String email) { + return KEY_PREFIX + email; + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java index aeb67d037..d6feed9e1 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java +++ b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java @@ -1,19 +1,20 @@ package com.example.solidconnection.auth.service; import static com.example.solidconnection.common.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; +import static com.example.solidconnection.common.exception.ErrorCode.SIGN_UP_TOKEN_INVALID; +import static com.example.solidconnection.common.exception.ErrorCode.USER_ALREADY_EXISTED; import com.example.solidconnection.auth.dto.SignInResponse; import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.common.exception.CustomException; -import com.example.solidconnection.location.country.domain.InterestedCountry; -import com.example.solidconnection.location.country.repository.CountryRepository; -import com.example.solidconnection.location.country.repository.InterestedCountryRepository; -import com.example.solidconnection.location.region.domain.InterestedRegion; -import com.example.solidconnection.location.region.repository.InterestedRegionRepository; -import com.example.solidconnection.location.region.repository.RegionRepository; +import com.example.solidconnection.location.country.service.InterestedCountryService; +import com.example.solidconnection.location.region.service.InterestedRegionService; +import com.example.solidconnection.siteuser.domain.AuthType; +import com.example.solidconnection.siteuser.domain.Role; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /* @@ -23,69 +24,77 @@ * - 관심 국가와 지역은 site_user_id를 참조하므로, 사용자 저장 후 저장한다. * - 바로 로그인하도록 액세스 토큰과 리프레시 토큰을 발급한다. * */ -public abstract class SignUpService { +@Service +@RequiredArgsConstructor +public class SignUpService { - protected final SignInService signInService; - protected final SiteUserRepository siteUserRepository; - protected final RegionRepository regionRepository; - protected final InterestedRegionRepository interestedRegionRepository; - protected final CountryRepository countryRepository; - protected final InterestedCountryRepository interestedCountryRepository; - - protected SignUpService(SignInService signInService, SiteUserRepository siteUserRepository, - RegionRepository regionRepository, InterestedRegionRepository interestedRegionRepository, - CountryRepository countryRepository, InterestedCountryRepository interestedCountryRepository) { - this.signInService = signInService; - this.siteUserRepository = siteUserRepository; - this.regionRepository = regionRepository; - this.interestedRegionRepository = interestedRegionRepository; - this.countryRepository = countryRepository; - this.interestedCountryRepository = interestedCountryRepository; - } + private final SignInService signInService; + private final SiteUserRepository siteUserRepository; + private final InterestedRegionService interestedRegionService; + private final InterestedCountryService interestedCountryService; + private final SignUpTokenProvider signUpTokenProvider; + private final PasswordTemporaryStorage passwordTemporaryStorage; @Transactional public SignInResponse signUp(SignUpRequest signUpRequest) { // 검증 - validateSignUpToken(signUpRequest); - validateUserNotDuplicated(signUpRequest); - validateNicknameDuplicated(signUpRequest.nickname()); + signUpTokenProvider.validateSignUpToken(signUpRequest.signUpToken()); + String email = signUpTokenProvider.parseEmail(signUpRequest.signUpToken()); + AuthType authType = signUpTokenProvider.parseAuthType(signUpRequest.signUpToken()); + validateNicknameNotDuplicated(signUpRequest.nickname()); + validateUserNotDuplicated(email, authType); + + // 임시 저장된 비밀번호 가져오기 + String password = getTemporarySavedPassword(email, authType); // 사용자 저장 - SiteUser siteUser = siteUserRepository.save(createSiteUser(signUpRequest)); + SiteUser siteUser = siteUserRepository.save(new SiteUser( + email, + signUpRequest.nickname(), + signUpRequest.profileImageUrl(), + signUpRequest.exchangeStatus(), + Role.MENTEE, + authType, + password + )); // 관심 지역, 국가 저장 - saveInterestedRegion(signUpRequest, siteUser); - saveInterestedCountry(signUpRequest, siteUser); + interestedRegionService.saveInterestedRegion(siteUser, signUpRequest.interestedRegions()); + interestedCountryService.saveInterestedCountry(siteUser, signUpRequest.interestedCountries()); // 로그인 - return signInService.signIn(siteUser); + SignInResponse response = signInService.signIn(siteUser); + + // 회원가입을 위해 저장한 데이터(SignUpToken, 비밀번호) 삭제 + clearSignUpData(email, authType); + + return response; } - private void validateNicknameDuplicated(String nickname) { + private void validateNicknameNotDuplicated(String nickname) { if (siteUserRepository.existsByNickname(nickname)) { throw new CustomException(NICKNAME_ALREADY_EXISTED); } } - private void saveInterestedRegion(SignUpRequest signUpRequest, SiteUser savedSiteUser) { - List interestedRegionNames = signUpRequest.interestedRegions(); - List interestedRegions = regionRepository.findByKoreanNames(interestedRegionNames).stream() - .map(region -> new InterestedRegion(savedSiteUser, region)) - .toList(); - interestedRegionRepository.saveAll(interestedRegions); + private void validateUserNotDuplicated(String email, AuthType authType) { + if (siteUserRepository.existsByEmailAndAuthType(email, authType)) { + throw new CustomException(USER_ALREADY_EXISTED); + } } - private void saveInterestedCountry(SignUpRequest signUpRequest, SiteUser savedSiteUser) { - List interestedCountryNames = signUpRequest.interestedCountries(); - List interestedCountries = countryRepository.findByKoreanNames(interestedCountryNames).stream() - .map(country -> new InterestedCountry(savedSiteUser, country)) - .toList(); - interestedCountryRepository.saveAll(interestedCountries); + private String getTemporarySavedPassword(String email, AuthType authType) { + if (authType == AuthType.EMAIL) { + return passwordTemporaryStorage.findByEmail(email) + .orElseThrow(() -> new CustomException(SIGN_UP_TOKEN_INVALID)); + } + return null; } - protected abstract void validateSignUpToken(SignUpRequest signUpRequest); - - protected abstract void validateUserNotDuplicated(SignUpRequest signUpRequest); - - protected abstract SiteUser createSiteUser(SignUpRequest signUpRequest); + private void clearSignUpData(String email, AuthType authType) { + if (authType == AuthType.EMAIL) { + passwordTemporaryStorage.deleteByEmail(email); + } + signUpTokenProvider.deleteByEmail(email); + } } diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java similarity index 87% rename from src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpTokenProvider.java rename to src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java index ae359c5b8..05480b10d 100644 --- a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpTokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java @@ -1,10 +1,9 @@ -package com.example.solidconnection.auth.service.oauth; +package com.example.solidconnection.auth.service; import static com.example.solidconnection.common.exception.ErrorCode.SIGN_UP_TOKEN_INVALID; import static com.example.solidconnection.common.exception.ErrorCode.SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER; import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.auth.service.TokenProvider; import com.example.solidconnection.auth.token.config.JwtProperties; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.siteuser.domain.AuthType; @@ -21,9 +20,9 @@ @Component @RequiredArgsConstructor -public class OAuthSignUpTokenProvider { +public class SignUpTokenProvider { - static final String AUTH_TYPE_CLAIM_KEY = "authType"; + private static final String AUTH_TYPE_CLAIM_KEY = "authType"; private final JwtProperties jwtProperties; private final RedisTemplate redisTemplate; @@ -44,13 +43,18 @@ public String generateAndSaveSignUpToken(String email, AuthType authType) { return tokenProvider.saveToken(signUpToken, TokenType.SIGN_UP); } + public void deleteByEmail(String email) { + String key = TokenType.SIGN_UP.addPrefix(email); + redisTemplate.delete(key); + } + public void validateSignUpToken(String token) { validateFormatAndExpiration(token); String email = parseEmail(token); validateIssuedByServer(email); } - private void validateFormatAndExpiration(String token) { + private void validateFormatAndExpiration(String token) { // 파싱되는지, AuthType이 포함되어있는지 검증 try { Claims claims = tokenProvider.parseClaims(token); Objects.requireNonNull(claims.getSubject()); diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java index bb34a3739..9343bfa21 100644 --- a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java @@ -7,6 +7,7 @@ import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto; import com.example.solidconnection.auth.dto.oauth.SignUpPrepareResponse; import com.example.solidconnection.auth.service.SignInService; +import com.example.solidconnection.auth.service.SignUpTokenProvider; import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -24,7 +25,7 @@ @RequiredArgsConstructor public class OAuthService { - private final OAuthSignUpTokenProvider OAuthSignUpTokenProvider; + private final SignUpTokenProvider signUpTokenProvider; private final SignInService signInService; private final SiteUserRepository siteUserRepository; private final OAuthClientMap oauthClientMap; @@ -49,7 +50,7 @@ private OAuthSignInResponse getSignInResponse(SiteUser siteUser) { } private SignUpPrepareResponse getSignUpPrepareResponse(OAuthUserInfoDto userInfoDto, AuthType authType) { - String signUpToken = OAuthSignUpTokenProvider.generateAndSaveSignUpToken(userInfoDto.getEmail(), authType); + String signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(userInfoDto.getEmail(), authType); return SignUpPrepareResponse.of(userInfoDto, signUpToken); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpService.java deleted file mode 100644 index ca50442fc..000000000 --- a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpService.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.example.solidconnection.auth.service.oauth; - -import static com.example.solidconnection.common.exception.ErrorCode.USER_ALREADY_EXISTED; - -import com.example.solidconnection.auth.dto.SignUpRequest; -import com.example.solidconnection.auth.service.SignInService; -import com.example.solidconnection.auth.service.SignUpService; -import com.example.solidconnection.common.exception.CustomException; -import com.example.solidconnection.location.country.repository.CountryRepository; -import com.example.solidconnection.location.country.repository.InterestedCountryRepository; -import com.example.solidconnection.location.region.repository.InterestedRegionRepository; -import com.example.solidconnection.location.region.repository.RegionRepository; -import com.example.solidconnection.siteuser.domain.AuthType; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import org.springframework.stereotype.Service; - -@Service -public class OAuthSignUpService extends SignUpService { - - private final OAuthSignUpTokenProvider oAuthSignUpTokenProvider; - - OAuthSignUpService(SignInService signInService, SiteUserRepository siteUserRepository, - RegionRepository regionRepository, InterestedRegionRepository interestedRegionRepository, - CountryRepository countryRepository, InterestedCountryRepository interestedCountryRepository, - OAuthSignUpTokenProvider oAuthSignUpTokenProvider) { - super(signInService, siteUserRepository, regionRepository, interestedRegionRepository, countryRepository, interestedCountryRepository); - this.oAuthSignUpTokenProvider = oAuthSignUpTokenProvider; - } - - @Override - protected void validateSignUpToken(SignUpRequest signUpRequest) { - oAuthSignUpTokenProvider.validateSignUpToken(signUpRequest.signUpToken()); - } - - @Override - protected void validateUserNotDuplicated(SignUpRequest signUpRequest) { - String email = oAuthSignUpTokenProvider.parseEmail(signUpRequest.signUpToken()); - AuthType authType = oAuthSignUpTokenProvider.parseAuthType(signUpRequest.signUpToken()); - if (siteUserRepository.existsByEmailAndAuthType(email, authType)) { - throw new CustomException(USER_ALREADY_EXISTED); - } - } - - @Override - protected SiteUser createSiteUser(SignUpRequest signUpRequest) { - String email = oAuthSignUpTokenProvider.parseEmail(signUpRequest.signUpToken()); - AuthType authType = oAuthSignUpTokenProvider.parseAuthType(signUpRequest.signUpToken()); - return signUpRequest.toOAuthSiteUser(email, authType); - } -} diff --git a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java index b0b04829b..80b833a9c 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -56,6 +56,7 @@ public enum ErrorCode { ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "액세스 토큰이 만료되었습니다. 재발급 api를 호출해주세요."), REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "리프레시 토큰이 만료되었습니다. 다시 로그인을 진행해주세요."), ACCESS_DENIED(HttpStatus.FORBIDDEN.value(), "접근 권한이 없습니다."), + SIGN_IN_FAILED(HttpStatus.UNAUTHORIZED.value(), "로그인에 실패했습니다. 이메일과 비밀번호를 확인해주세요."), // s3 S3_SERVICE_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "S3 서비스 에러 발생"), diff --git a/src/main/java/com/example/solidconnection/location/country/repository/CountryRepository.java b/src/main/java/com/example/solidconnection/location/country/repository/CountryRepository.java index 70477f1f4..5ba92f80a 100644 --- a/src/main/java/com/example/solidconnection/location/country/repository/CountryRepository.java +++ b/src/main/java/com/example/solidconnection/location/country/repository/CountryRepository.java @@ -3,11 +3,8 @@ import com.example.solidconnection.location.country.domain.Country; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; public interface CountryRepository extends JpaRepository { - @Query("SELECT c FROM Country c WHERE c.koreanName IN :names") - List findByKoreanNames(@Param(value = "names") List names); + List findAllByKoreanNameIn(List koreanNames); } diff --git a/src/main/java/com/example/solidconnection/location/country/service/InterestedCountryService.java b/src/main/java/com/example/solidconnection/location/country/service/InterestedCountryService.java new file mode 100644 index 000000000..8166de968 --- /dev/null +++ b/src/main/java/com/example/solidconnection/location/country/service/InterestedCountryService.java @@ -0,0 +1,27 @@ +package com.example.solidconnection.location.country.service; + +import com.example.solidconnection.location.country.domain.InterestedCountry; +import com.example.solidconnection.location.country.repository.CountryRepository; +import com.example.solidconnection.location.country.repository.InterestedCountryRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class InterestedCountryService { + + private final CountryRepository countryRepository; + private final InterestedCountryRepository interestedCountryRepository; + + @Transactional + public void saveInterestedCountry(SiteUser siteUser, List koreanNames) { + List interestedCountries = countryRepository.findAllByKoreanNameIn(koreanNames) + .stream() + .map(country -> new InterestedCountry(siteUser, country)) + .toList(); + interestedCountryRepository.saveAll(interestedCountries); + } +} diff --git a/src/main/java/com/example/solidconnection/location/region/repository/RegionRepository.java b/src/main/java/com/example/solidconnection/location/region/repository/RegionRepository.java index 656fc4377..dea93fb34 100644 --- a/src/main/java/com/example/solidconnection/location/region/repository/RegionRepository.java +++ b/src/main/java/com/example/solidconnection/location/region/repository/RegionRepository.java @@ -4,13 +4,10 @@ import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; public interface RegionRepository extends JpaRepository { - @Query("SELECT r FROM Region r WHERE r.koreanName IN :names") - List findByKoreanNames(@Param(value = "names") List names); + List findAllByKoreanNameIn(List koreanNames); Optional findByKoreanName(String koreanName); } diff --git a/src/main/java/com/example/solidconnection/location/region/service/InterestedRegionService.java b/src/main/java/com/example/solidconnection/location/region/service/InterestedRegionService.java new file mode 100644 index 000000000..6dc71263e --- /dev/null +++ b/src/main/java/com/example/solidconnection/location/region/service/InterestedRegionService.java @@ -0,0 +1,27 @@ +package com.example.solidconnection.location.region.service; + +import com.example.solidconnection.location.region.domain.InterestedRegion; +import com.example.solidconnection.location.region.repository.InterestedRegionRepository; +import com.example.solidconnection.location.region.repository.RegionRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class InterestedRegionService { + + private final RegionRepository regionRepository; + private final InterestedRegionRepository interestedRegionRepository; + + @Transactional + public void saveInterestedRegion(SiteUser siteUser, List koreanNames) { + List interestedRegions = regionRepository.findAllByKoreanNameIn(koreanNames) + .stream() + .map(region -> new InterestedRegion(siteUser, region)) + .toList(); + interestedRegionRepository.saveAll(interestedRegions); + } +} diff --git a/src/test/java/com/example/solidconnection/auth/service/EmailSignInServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/EmailSignInServiceTest.java index f9d481311..04b6780ad 100644 --- a/src/test/java/com/example/solidconnection/auth/service/EmailSignInServiceTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/EmailSignInServiceTest.java @@ -55,7 +55,7 @@ class 로그인에_실패한다 { // when & then assertThatCode(() -> emailSignInService.signIn(signInRequest)) .isInstanceOf(CustomException.class) - .hasMessageContaining(ErrorCode.USER_NOT_FOUND.getMessage()); + .hasMessageContaining(ErrorCode.SIGN_IN_FAILED.getMessage()); } @Test @@ -68,7 +68,7 @@ class 로그인에_실패한다 { // when & then assertThatCode(() -> emailSignInService.signIn(signInRequest)) .isInstanceOf(CustomException.class) - .hasMessageContaining(ErrorCode.USER_NOT_FOUND.getMessage()); + .hasMessageContaining(ErrorCode.SIGN_IN_FAILED.getMessage()); } } } diff --git a/src/test/java/com/example/solidconnection/auth/service/PasswordTemporaryStorageTest.java b/src/test/java/com/example/solidconnection/auth/service/PasswordTemporaryStorageTest.java new file mode 100644 index 000000000..ea3ed6355 --- /dev/null +++ b/src/test/java/com/example/solidconnection/auth/service/PasswordTemporaryStorageTest.java @@ -0,0 +1,48 @@ +package com.example.solidconnection.auth.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.example.solidconnection.support.TestContainerSpringBootTest; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; + +@DisplayName("비밀번호 임시 저장소 테스트") +@TestContainerSpringBootTest +class PasswordTemporaryStorageTest { + + @Autowired + private PasswordTemporaryStorage passwordTemporaryStorage; + + @Autowired + private PasswordEncoder passwordEncoder; + + private final String email = "test@email.com"; + private final String rawPassword = "password123"; + + @Test + void 인코딩된_비밀번호를_임시_저장소에_저장하고_조회한다() { + // when + passwordTemporaryStorage.save(email, rawPassword); + Optional foundPassword = passwordTemporaryStorage.findByEmail(email); + + // then + assertThat(foundPassword).isPresent(); + assertThat(passwordEncoder.matches(rawPassword, foundPassword.get())).isTrue(); + } + + @Test + void 임시_저장된_비밀번호를_삭제한다() { + // given + passwordTemporaryStorage.save(email, rawPassword); + + // when + passwordTemporaryStorage.deleteByEmail(email); + Optional foundPassword = passwordTemporaryStorage.findByEmail(email); + + // then + assertThat(foundPassword).isEmpty(); + } +} diff --git a/src/test/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpTokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java similarity index 69% rename from src/test/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpTokenProviderTest.java rename to src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java index bd8833bb0..c75eac5f5 100644 --- a/src/test/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpTokenProviderTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java @@ -1,6 +1,5 @@ -package com.example.solidconnection.auth.service.oauth; +package com.example.solidconnection.auth.service; -import static com.example.solidconnection.auth.service.oauth.OAuthSignUpTokenProvider.AUTH_TYPE_CLAIM_KEY; import static com.example.solidconnection.common.exception.ErrorCode.SIGN_UP_TOKEN_INVALID; import static com.example.solidconnection.common.exception.ErrorCode.SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER; import static org.assertj.core.api.Assertions.assertThat; @@ -8,7 +7,6 @@ import static org.junit.jupiter.api.Assertions.assertAll; import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.auth.service.TokenProvider; import com.example.solidconnection.auth.token.config.JwtProperties; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.siteuser.domain.AuthType; @@ -27,11 +25,11 @@ import org.springframework.data.redis.core.RedisTemplate; @TestContainerSpringBootTest -@DisplayName("OAuth 회원가입 토큰 제공자 테스트") -class OAuthSignUpTokenProviderTest { +@DisplayName("회원가입 토큰 제공자 테스트") +class SignUpTokenProviderTest { @Autowired - private OAuthSignUpTokenProvider OAuthSignUpTokenProvider; + private SignUpTokenProvider signUpTokenProvider; @Autowired private TokenProvider tokenProvider; @@ -42,19 +40,19 @@ class OAuthSignUpTokenProviderTest { @Autowired private JwtProperties jwtProperties; + private final String authTypeClaimKey = "authType"; + private final String email = "test@email.com"; + private final AuthType authType = AuthType.KAKAO; + @Test void 회원가입_토큰을_생성하고_저장한다() { - // given - String email = "email"; - AuthType authType = AuthType.KAKAO; - // when - String signUpToken = OAuthSignUpTokenProvider.generateAndSaveSignUpToken(email, authType); + String signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(email, authType); // then Claims claims = tokenProvider.parseClaims(signUpToken); String actualSubject = claims.getSubject(); - AuthType actualAuthType = AuthType.valueOf(claims.get(AUTH_TYPE_CLAIM_KEY, String.class)); + AuthType actualAuthType = AuthType.valueOf(claims.get(authTypeClaimKey, String.class)); String signUpTokenKey = TokenType.SIGN_UP.addPrefix(email); assertAll( () -> assertThat(actualSubject).isEqualTo(email), @@ -63,19 +61,31 @@ class OAuthSignUpTokenProviderTest { ); } + @Test + void 회원가입_토큰을_삭제한다() { + // given + signUpTokenProvider.generateAndSaveSignUpToken(email, authType); + + // when + signUpTokenProvider.deleteByEmail(email); + + // then + String signUpTokenKey = TokenType.SIGN_UP.addPrefix(email); + assertThat(redisTemplate.opsForValue().get(signUpTokenKey)).isNull(); + } + @Nested class 주어진_회원가입_토큰을_검증한다 { @Test void 검증_성공한다() { // given - String email = "email@test.com"; - Map claim = new HashMap<>(Map.of(AUTH_TYPE_CLAIM_KEY, AuthType.APPLE)); + Map claim = new HashMap<>(Map.of(authTypeClaimKey, authType)); String validToken = createBaseJwtBuilder().setSubject(email).addClaims(claim).compact(); redisTemplate.opsForValue().set(TokenType.SIGN_UP.addPrefix(email), validToken); // when & then - assertThatCode(() -> OAuthSignUpTokenProvider.validateSignUpToken(validToken)).doesNotThrowAnyException(); + assertThatCode(() -> signUpTokenProvider.validateSignUpToken(validToken)).doesNotThrowAnyException(); } @Test @@ -84,7 +94,7 @@ class 주어진_회원가입_토큰을_검증한다 { String expiredToken = createExpiredToken(); // when & then - assertThatCode(() -> OAuthSignUpTokenProvider.validateSignUpToken(expiredToken)) + assertThatCode(() -> signUpTokenProvider.validateSignUpToken(expiredToken)) .isInstanceOf(CustomException.class) .hasMessageContaining(SIGN_UP_TOKEN_INVALID.getMessage()); } @@ -95,7 +105,7 @@ class 주어진_회원가입_토큰을_검증한다 { String notJwt = "not jwt"; // when & then - assertThatCode(() -> OAuthSignUpTokenProvider.validateSignUpToken(notJwt)) + assertThatCode(() -> signUpTokenProvider.validateSignUpToken(notJwt)) .isInstanceOf(CustomException.class) .hasMessageContaining(SIGN_UP_TOKEN_INVALID.getMessage()); } @@ -103,11 +113,12 @@ class 주어진_회원가입_토큰을_검증한다 { @Test void 정해진_형식에_맞지_않으면_예외가_발생한다_authType_클래스_불일치() { // given - Map wrongClaim = new HashMap<>(Map.of(AUTH_TYPE_CLAIM_KEY, "카카오")); - String wrongAuthType = createBaseJwtBuilder().addClaims(wrongClaim).compact(); + String wrongAuthType = "카카오"; + Map wrongClaim = new HashMap<>(Map.of(authTypeClaimKey, wrongAuthType)); + String wrongAuthTypeClaim = createBaseJwtBuilder().addClaims(wrongClaim).compact(); // when & then - assertThatCode(() -> OAuthSignUpTokenProvider.validateSignUpToken(wrongAuthType)) + assertThatCode(() -> signUpTokenProvider.validateSignUpToken(wrongAuthTypeClaim)) .isInstanceOf(CustomException.class) .hasMessageContaining(SIGN_UP_TOKEN_INVALID.getMessage()); } @@ -115,11 +126,11 @@ class 주어진_회원가입_토큰을_검증한다 { @Test void 정해진_형식에_맞지_않으면_예외가_발생한다_subject_누락() { // given - Map claim = new HashMap<>(Map.of(AUTH_TYPE_CLAIM_KEY, AuthType.APPLE)); + Map claim = new HashMap<>(Map.of(authTypeClaimKey, authType)); String noSubject = createBaseJwtBuilder().addClaims(claim).compact(); // when & then - assertThatCode(() -> OAuthSignUpTokenProvider.validateSignUpToken(noSubject)) + assertThatCode(() -> signUpTokenProvider.validateSignUpToken(noSubject)) .isInstanceOf(CustomException.class) .hasMessageContaining(SIGN_UP_TOKEN_INVALID.getMessage()); } @@ -127,11 +138,11 @@ class 주어진_회원가입_토큰을_검증한다 { @Test void 우리_서버에_발급된_토큰이_아니면_예외가_발생한다() { // given - Map validClaim = new HashMap<>(Map.of(AUTH_TYPE_CLAIM_KEY, AuthType.APPLE)); - String signUpToken = createBaseJwtBuilder().addClaims(validClaim).setSubject("email").compact(); + Map validClaim = new HashMap<>(Map.of(authTypeClaimKey, authType)); + String signUpToken = createBaseJwtBuilder().addClaims(validClaim).setSubject(email).compact(); // when & then - assertThatCode(() -> OAuthSignUpTokenProvider.validateSignUpToken(signUpToken)) + assertThatCode(() -> signUpTokenProvider.validateSignUpToken(signUpToken)) .isInstanceOf(CustomException.class) .hasMessageContaining(SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER.getMessage()); } @@ -140,13 +151,12 @@ class 주어진_회원가입_토큰을_검증한다 { @Test void 회원가입_토큰에서_이메일을_추출한다() { // given - String email = "email@test.com"; - Map claim = Map.of(AUTH_TYPE_CLAIM_KEY, AuthType.APPLE); + Map claim = Map.of(authTypeClaimKey, authType); String validToken = createBaseJwtBuilder().setSubject(email).addClaims(claim).compact(); redisTemplate.opsForValue().set(TokenType.SIGN_UP.addPrefix(email), validToken); // when - String extractedEmail = OAuthSignUpTokenProvider.parseEmail(validToken); + String extractedEmail = signUpTokenProvider.parseEmail(validToken); // then assertThat(extractedEmail).isEqualTo(email); @@ -155,12 +165,11 @@ class 주어진_회원가입_토큰을_검증한다 { @Test void 회원가입_토큰에서_인증_타입을_추출한다() { // given - AuthType authType = AuthType.APPLE; - Map claim = Map.of(AUTH_TYPE_CLAIM_KEY, authType); - String validToken = createBaseJwtBuilder().setSubject("email").addClaims(claim).compact(); + Map claim = Map.of(authTypeClaimKey, authType); + String validToken = createBaseJwtBuilder().setSubject(email).addClaims(claim).compact(); // when - AuthType extractedAuthType = OAuthSignUpTokenProvider.parseAuthType(validToken); + AuthType extractedAuthType = signUpTokenProvider.parseAuthType(validToken); // then assertThat(extractedAuthType).isEqualTo(authType); @@ -168,7 +177,7 @@ class 주어진_회원가입_토큰을_검증한다 { private String createExpiredToken() { return Jwts.builder() - .setSubject("subject") + .setSubject(email) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() - 1000)) .signWith(SignatureAlgorithm.HS256, jwtProperties.secret())