diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index 1712ec5..ba7d703 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -34,4 +34,4 @@ jobs: working-directory: ${{ env.working-directory }} run: | chmod +x gradlew - ./gradlew clean build \ No newline at end of file + ./gradlew clean build -Dspring.profiles.active=dev \ No newline at end of file diff --git a/.github/workflows/prod-ci.yml b/.github/workflows/prod-ci.yml index a5bd96b..ed32f24 100644 --- a/.github/workflows/prod-ci.yml +++ b/.github/workflows/prod-ci.yml @@ -34,4 +34,4 @@ jobs: working-directory: ${{ env.working-directory }} run: | chmod +x gradlew - ./gradlew clean build \ No newline at end of file + ./gradlew clean build -Dspring.profiles.active=prod \ No newline at end of file diff --git a/purithm/src/main/java/com/example/purithm/domain/user/entity/Provider.java b/purithm/src/main/java/com/example/purithm/domain/user/entity/Provider.java index 7a91d24..6053127 100644 --- a/purithm/src/main/java/com/example/purithm/domain/user/entity/Provider.java +++ b/purithm/src/main/java/com/example/purithm/domain/user/entity/Provider.java @@ -5,6 +5,7 @@ @AllArgsConstructor public enum Provider { KAKAO("KAKAO"), - APPLE("APPLE"); + APPLE("APPLE"), + PURITHM("PURITHM"); private final String provider; } diff --git a/purithm/src/main/java/com/example/purithm/domain/user/entity/User.java b/purithm/src/main/java/com/example/purithm/domain/user/entity/User.java index f4f4490..4dd247c 100644 --- a/purithm/src/main/java/com/example/purithm/domain/user/entity/User.java +++ b/purithm/src/main/java/com/example/purithm/domain/user/entity/User.java @@ -47,6 +47,8 @@ public class User { private String email; + private String password; + @Temporal(TemporalType.TIMESTAMP) @CreatedDate @Column(updatable = false) diff --git a/purithm/src/main/java/com/example/purithm/domain/user/repository/UserRepository.java b/purithm/src/main/java/com/example/purithm/domain/user/repository/UserRepository.java index 597e703..b1b7dc2 100644 --- a/purithm/src/main/java/com/example/purithm/domain/user/repository/UserRepository.java +++ b/purithm/src/main/java/com/example/purithm/domain/user/repository/UserRepository.java @@ -1,5 +1,7 @@ package com.example.purithm.domain.user.repository; +import java.util.Optional; + import com.example.purithm.domain.user.entity.Provider; import com.example.purithm.domain.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; @@ -17,4 +19,8 @@ public interface UserRepository extends JpaRepository { @Query("SELECT COUNT(ufl) FROM UserFilterLog ufl WHERE ufl.userId = :userId") int countLogsByUserId(@Param("userId") Long userId); + + boolean existsByProviderId(String id); + + Optional findByProviderId(String id); } diff --git a/purithm/src/main/java/com/example/purithm/domain/user/service/UserService.java b/purithm/src/main/java/com/example/purithm/domain/user/service/UserService.java index 1a371f9..5d9434c 100644 --- a/purithm/src/main/java/com/example/purithm/domain/user/service/UserService.java +++ b/purithm/src/main/java/com/example/purithm/domain/user/service/UserService.java @@ -4,7 +4,9 @@ import com.example.purithm.domain.user.dto.request.UserInfoRequestDto; import com.example.purithm.domain.user.dto.response.AccountInfoDto; import com.example.purithm.domain.user.dto.response.UserInfoDto; -import com.example.purithm.global.auth.dto.response.SocialUserInfoDto; +import com.example.purithm.domain.user.entity.Provider; +import com.example.purithm.global.auth.dto.request.LoginRequestDto; +import com.example.purithm.global.auth.dto.response.SignUpUserInfoDto; import com.example.purithm.domain.user.entity.User; import com.example.purithm.domain.user.repository.UserRepository; import com.example.purithm.global.exception.CustomException; @@ -13,6 +15,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @Slf4j @@ -20,8 +23,14 @@ @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + public Long signUp(SignUpUserInfoDto socialUserInfoDto) { + if (socialUserInfoDto.getProvider().equals(Provider.PURITHM) + && userRepository.existsByProviderId(socialUserInfoDto.getProviderId())) { + throw CustomException.of(Error.NICKNAME_ALREADY_USED_ERROR); + } - public Long signUp(SocialUserInfoDto socialUserInfoDto) { User existUser = userRepository .findByProviderAndProviderId(socialUserInfoDto.getProvider(), socialUserInfoDto.getProviderId()); @@ -37,6 +46,7 @@ public Long signUp(SocialUserInfoDto socialUserInfoDto) { .email(socialUserInfoDto.getEmail()) .terms(false) .membership(Membership.BASIC) + .password(passwordEncoder.encode(socialUserInfoDto.getPassword())) .build(); User savedUser = userRepository.save(user); @@ -92,4 +102,14 @@ public void updateProfile(UserInfoRequestDto userInfo, Long userId) { user.updateProfile(userInfo); userRepository.save(user); } + + public Long getUserId(LoginRequestDto loginRequestDto) { + User user = userRepository.findByProviderId(loginRequestDto.id()) + .orElseThrow(() -> CustomException.of(Error.NOT_FOUND_ERROR)); + + if (!passwordEncoder.matches(loginRequestDto.password(), user.getPassword())) { + throw CustomException.of(Error.INVALID_ID_PASSWORD); + } + return user.getId(); + } } diff --git a/purithm/src/main/java/com/example/purithm/global/auth/config/LoginConfig.java b/purithm/src/main/java/com/example/purithm/global/auth/config/LoginConfig.java index cbbae00..f0306ad 100644 --- a/purithm/src/main/java/com/example/purithm/global/auth/config/LoginConfig.java +++ b/purithm/src/main/java/com/example/purithm/global/auth/config/LoginConfig.java @@ -3,7 +3,11 @@ import com.example.purithm.global.auth.argumentresolver.LoginUserArgumentResolver; import java.util.List; import lombok.RequiredArgsConstructor; + +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -15,4 +19,9 @@ public class LoginConfig implements WebMvcConfigurer { public void addArgumentResolvers(List argumentResolvers) { argumentResolvers.add(loginUserArgumentResolver); } + + @Bean + public PasswordEncoder passwordEncoder(){ + return new BCryptPasswordEncoder(); + } } diff --git a/purithm/src/main/java/com/example/purithm/global/auth/controller/AuthController.java b/purithm/src/main/java/com/example/purithm/global/auth/controller/AuthController.java index e4acad9..cbde6db 100644 --- a/purithm/src/main/java/com/example/purithm/global/auth/controller/AuthController.java +++ b/purithm/src/main/java/com/example/purithm/global/auth/controller/AuthController.java @@ -1,9 +1,11 @@ package com.example.purithm.global.auth.controller; import com.example.purithm.domain.user.entity.Provider; +import com.example.purithm.global.auth.dto.request.LoginRequestDto; +import com.example.purithm.global.auth.dto.request.SignUpRequestDto; import com.example.purithm.global.auth.dto.response.KakaoUserInfoDto; import com.example.purithm.global.auth.dto.response.LoginDto; -import com.example.purithm.global.auth.dto.response.SocialUserInfoDto; +import com.example.purithm.global.auth.dto.response.SignUpUserInfoDto; import com.example.purithm.global.auth.jwt.JWTUtil; import com.example.purithm.global.config.WebClientConfig; import com.example.purithm.domain.user.service.UserService; @@ -17,6 +19,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -33,6 +36,32 @@ public class AuthController implements AuthControllerDocs { private final UserService userService; private final JWTUtil jwtUtil; + @PostMapping("/login") + public SuccessResponse login(LoginRequestDto loginRequestDto) { + Long id = userService.getUserId(loginRequestDto); + String jwtToken = jwtUtil.createJwt(id, 60 * 60 * 60 * 1000L); + + LoginDto loginDto = LoginDto.builder() + .accessToken(jwtToken).build(); + return SuccessResponse.of(loginDto); + } + + @PostMapping("/signup") + public SuccessResponse signup(SignUpRequestDto signUpRequestDto) { + SignUpUserInfoDto userInfoDto = SignUpUserInfoDto.builder() + .profile(signUpRequestDto.profile()) + .username(signUpRequestDto.username()) + .provider(Provider.PURITHM) + .providerId(signUpRequestDto.id()) + .email(signUpRequestDto.email()) + .password(signUpRequestDto.password()) + .build(); + + Long id = userService.signUp(userInfoDto); + + return SuccessResponse.of(); + } + @GetMapping("/kakao") public Mono> kakaoLogin(String token) { return webClientConfig.webClient() @@ -45,13 +74,14 @@ public Mono> kakaoLogin(String token) { .bodyToMono(KakaoUserInfoDto.class) .flatMap(res -> { - SocialUserInfoDto userInfoDto = SocialUserInfoDto.builder() - .profile(res.getProperties().getProfile_image()) - .username(res.getProperties().getNickname()) - .provider(Provider.KAKAO) - .providerId(String.valueOf(res.getId())) - .email(res.getKakao_account().getEmail()) - .build(); + SignUpUserInfoDto userInfoDto = SignUpUserInfoDto.builder() + .profile(res.getProperties().getProfile_image()) + .username(res.getProperties().getNickname()) + .provider(Provider.KAKAO) + .providerId(String.valueOf(res.getId())) + .email(res.getKakao_account().getEmail()) + .password(null) + .build(); Long id = userService.signUp(userInfoDto); String jwtToken = jwtUtil.createJwt(id, 60 * 60 * 60 * 1000L); @@ -72,12 +102,13 @@ public SuccessResponse appleLogin(String token, String username) try { token = token.substring(7); Claims claims = jwtUtil.getAppleTokenClaims(token); - SocialUserInfoDto userInfoDto = SocialUserInfoDto.builder() + SignUpUserInfoDto userInfoDto = SignUpUserInfoDto.builder() .profile(null) .username(username) .provider(Provider.APPLE) .providerId((String) claims.get("sub")) .email((String) claims.get("email")) + .password(null) .build(); Long id = userService.signUp(userInfoDto); diff --git a/purithm/src/main/java/com/example/purithm/global/auth/controller/AuthControllerDocs.java b/purithm/src/main/java/com/example/purithm/global/auth/controller/AuthControllerDocs.java index 4f6f9fe..072abed 100644 --- a/purithm/src/main/java/com/example/purithm/global/auth/controller/AuthControllerDocs.java +++ b/purithm/src/main/java/com/example/purithm/global/auth/controller/AuthControllerDocs.java @@ -1,5 +1,7 @@ package com.example.purithm.global.auth.controller; +import com.example.purithm.global.auth.dto.request.LoginRequestDto; +import com.example.purithm.global.auth.dto.request.SignUpRequestDto; import com.example.purithm.global.auth.dto.response.LoginDto; import com.example.purithm.global.response.SuccessResponse; import com.nimbusds.jose.JOSEException; @@ -9,27 +11,37 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.io.IOException; import java.text.ParseException; + +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; import reactor.core.publisher.Mono; public interface AuthControllerDocs { - @Operation( + @Operation( + summary = "자체 회원 가입" + ) + public SuccessResponse signup(@RequestBody SignUpRequestDto signUpRequestDto); + + @Operation( + summary = "자체 로그인" + ) + public SuccessResponse login(@RequestBody LoginRequestDto loginRequestDto); + + @Operation( summary = "Kakao Login", parameters = { @Parameter(name = "Authorization", description = "kakao access token을 보냅니다. Bearer token 형식입니다.", required = true, in = ParameterIn.HEADER) - } - ) - public Mono> kakaoLogin(@RequestHeader("Authorization") String token); + }) + public Mono> kakaoLogin(@RequestHeader("Authorization") String token); - @Operation( + @Operation( summary = "Apple Login", parameters = { @Parameter(name = "Authorization", description = "Apple access token을 보냅니다. Bearer token 형식입니다.", required = true, in = ParameterIn.HEADER, schema = @Schema(type = "string")) - } - ) - public SuccessResponse appleLogin( + }) + public SuccessResponse appleLogin( @RequestHeader("Authorization") String token, @RequestParam(value = "username", required = false) String username) throws IOException, ParseException, JOSEException; } diff --git a/purithm/src/main/java/com/example/purithm/global/auth/dto/request/LoginRequestDto.java b/purithm/src/main/java/com/example/purithm/global/auth/dto/request/LoginRequestDto.java new file mode 100644 index 0000000..8069406 --- /dev/null +++ b/purithm/src/main/java/com/example/purithm/global/auth/dto/request/LoginRequestDto.java @@ -0,0 +1,11 @@ +package com.example.purithm.global.auth.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record LoginRequestDto( + @Schema(description = "id") + String id, + @Schema(description = "password") + String password +) { +} diff --git a/purithm/src/main/java/com/example/purithm/global/auth/dto/request/SignUpRequestDto.java b/purithm/src/main/java/com/example/purithm/global/auth/dto/request/SignUpRequestDto.java new file mode 100644 index 0000000..6ade255 --- /dev/null +++ b/purithm/src/main/java/com/example/purithm/global/auth/dto/request/SignUpRequestDto.java @@ -0,0 +1,17 @@ +package com.example.purithm.global.auth.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record SignUpRequestDto( + @Schema(description = "회원 id") + String id, + @Schema(description = "닉네임") + String username, + @Schema(description = "비밀 번호") + String password, + @Schema(description = "프로필 사진") + String profile, + @Schema(description = "email") + String email +) { +} diff --git a/purithm/src/main/java/com/example/purithm/global/auth/dto/response/SocialUserInfoDto.java b/purithm/src/main/java/com/example/purithm/global/auth/dto/response/SignUpUserInfoDto.java similarity index 83% rename from purithm/src/main/java/com/example/purithm/global/auth/dto/response/SocialUserInfoDto.java rename to purithm/src/main/java/com/example/purithm/global/auth/dto/response/SignUpUserInfoDto.java index 3698d5e..16c2763 100644 --- a/purithm/src/main/java/com/example/purithm/global/auth/dto/response/SocialUserInfoDto.java +++ b/purithm/src/main/java/com/example/purithm/global/auth/dto/response/SignUpUserInfoDto.java @@ -7,10 +7,11 @@ @Getter @Builder -public class SocialUserInfoDto { +public class SignUpUserInfoDto { private String username; private Provider provider; private String providerId; private String profile; private String email; + private String password; } diff --git a/purithm/src/main/java/com/example/purithm/global/exception/Error.java b/purithm/src/main/java/com/example/purithm/global/exception/Error.java index c9236e9..ee46c94 100644 --- a/purithm/src/main/java/com/example/purithm/global/exception/Error.java +++ b/purithm/src/main/java/com/example/purithm/global/exception/Error.java @@ -16,6 +16,7 @@ public enum Error { /* 401 */ INVALID_TOKEN_ERROR(HttpStatus.UNAUTHORIZED, 40100, "유효하지 않은 토큰입니다."), EXPIRED_TOKEN_ERROR(HttpStatus.UNAUTHORIZED, 40101, "만료된 토큰입니다."), + INVALID_ID_PASSWORD(HttpStatus.UNAUTHORIZED, 40102, "아이디/비밀번호가 적절하지 않습니다."), /* 403 */ NOT_AGREED_TERM(HttpStatus.FORBIDDEN, 40300, "이용약관 동의가 필요합니다"),