diff --git a/src/main/java/org/gachon/checkmate/domain/member/controller/MemberController.java b/src/main/java/org/gachon/checkmate/domain/member/controller/MemberController.java index 58e883f..befbfff 100644 --- a/src/main/java/org/gachon/checkmate/domain/member/controller/MemberController.java +++ b/src/main/java/org/gachon/checkmate/domain/member/controller/MemberController.java @@ -2,14 +2,16 @@ import lombok.RequiredArgsConstructor; import org.gachon.checkmate.domain.member.dto.request.EmailPostRequestDto; +import org.gachon.checkmate.domain.member.dto.request.MemberSignInRequestDto; +import org.gachon.checkmate.domain.member.dto.request.MemberSignUpRequestDto; +import org.gachon.checkmate.domain.member.dto.request.PasswordResetRequestDto; import org.gachon.checkmate.domain.member.dto.response.EmailResponseDto; +import org.gachon.checkmate.domain.member.dto.response.MemberSignInResponseDto; +import org.gachon.checkmate.domain.member.dto.response.MemberSignUpResponseDto; import org.gachon.checkmate.domain.member.service.MemberService; import org.gachon.checkmate.global.common.SuccessResponse; import org.springframework.http.ResponseEntity; -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.RestController; +import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor @RequestMapping("/api/member") @@ -23,4 +25,22 @@ public ResponseEntity> sendMail(@RequestBody final EmailPostR final EmailResponseDto emailResponseDto = memberService.sendMail(emailPostRequestDto); return SuccessResponse.ok(emailResponseDto); } + + @PostMapping("/signup") + public ResponseEntity> signUp(@RequestBody final MemberSignUpRequestDto memberSignUpRequestDto){ + final MemberSignUpResponseDto memberSignUpResponseDto = memberService.signUp(memberSignUpRequestDto); + return SuccessResponse.created(memberSignUpResponseDto); + } + + @PostMapping("/signin") + public ResponseEntity> signIn(@RequestBody final MemberSignInRequestDto memberSignInRequestDto){ + final MemberSignInResponseDto memberSignInResponseDto = memberService.signIn(memberSignInRequestDto); + return SuccessResponse.ok(memberSignInResponseDto); + } + + @PatchMapping("/reset") + public ResponseEntity> setPassword(@RequestBody final PasswordResetRequestDto passwordResetRequestDto){ + memberService.setPassword(passwordResetRequestDto); + return SuccessResponse.ok(null); + } } diff --git a/src/main/java/org/gachon/checkmate/domain/member/dto/request/MemberSignInRequestDto.java b/src/main/java/org/gachon/checkmate/domain/member/dto/request/MemberSignInRequestDto.java new file mode 100644 index 0000000..153d60a --- /dev/null +++ b/src/main/java/org/gachon/checkmate/domain/member/dto/request/MemberSignInRequestDto.java @@ -0,0 +1,7 @@ +package org.gachon.checkmate.domain.member.dto.request; + +public record MemberSignInRequestDto( + String email, + String password +) { +} diff --git a/src/main/java/org/gachon/checkmate/domain/member/dto/request/MemberSignUpRequestDto.java b/src/main/java/org/gachon/checkmate/domain/member/dto/request/MemberSignUpRequestDto.java new file mode 100644 index 0000000..9df7b54 --- /dev/null +++ b/src/main/java/org/gachon/checkmate/domain/member/dto/request/MemberSignUpRequestDto.java @@ -0,0 +1,15 @@ +package org.gachon.checkmate.domain.member.dto.request; + +import org.gachon.checkmate.domain.member.entity.GenderType; +import org.gachon.checkmate.domain.member.entity.MbtiType; + +public record MemberSignUpRequestDto( + String email, + String password, + String name, + String school, + String major, + MbtiType mbtiType, + GenderType genderType +) { +} diff --git a/src/main/java/org/gachon/checkmate/domain/member/dto/request/PasswordResetRequestDto.java b/src/main/java/org/gachon/checkmate/domain/member/dto/request/PasswordResetRequestDto.java new file mode 100644 index 0000000..2dcb57f --- /dev/null +++ b/src/main/java/org/gachon/checkmate/domain/member/dto/request/PasswordResetRequestDto.java @@ -0,0 +1,7 @@ +package org.gachon.checkmate.domain.member.dto.request; + +public record PasswordResetRequestDto( + String email, + String newPassword +) { +} diff --git a/src/main/java/org/gachon/checkmate/domain/member/dto/response/MemberSignInResponseDto.java b/src/main/java/org/gachon/checkmate/domain/member/dto/response/MemberSignInResponseDto.java new file mode 100644 index 0000000..b594764 --- /dev/null +++ b/src/main/java/org/gachon/checkmate/domain/member/dto/response/MemberSignInResponseDto.java @@ -0,0 +1,20 @@ +package org.gachon.checkmate.domain.member.dto.response; + +import lombok.Builder; + +@Builder +public record MemberSignInResponseDto( + Long memberId, + String accessToken, + String refreshToken +) { + public static MemberSignInResponseDto of(Long memberId, + String accessToken, + String refreshToken) { + return MemberSignInResponseDto.builder() + .memberId(memberId) + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } +} diff --git a/src/main/java/org/gachon/checkmate/domain/member/dto/response/MemberSignUpResponseDto.java b/src/main/java/org/gachon/checkmate/domain/member/dto/response/MemberSignUpResponseDto.java new file mode 100644 index 0000000..052daa9 --- /dev/null +++ b/src/main/java/org/gachon/checkmate/domain/member/dto/response/MemberSignUpResponseDto.java @@ -0,0 +1,23 @@ +package org.gachon.checkmate.domain.member.dto.response; + +import lombok.Builder; + +@Builder +public record MemberSignUpResponseDto( + Long memberId, + String name, + String accessToken, + String refreshToken +) { + public static MemberSignUpResponseDto of(Long memberId, + String name, + String accessToken, + String refreshToken) { + return MemberSignUpResponseDto.builder() + .memberId(memberId) + .name(name) + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } +} diff --git a/src/main/java/org/gachon/checkmate/domain/member/entity/ProfileImageType.java b/src/main/java/org/gachon/checkmate/domain/member/entity/ProfileImageType.java new file mode 100644 index 0000000..052524c --- /dev/null +++ b/src/main/java/org/gachon/checkmate/domain/member/entity/ProfileImageType.java @@ -0,0 +1,16 @@ +package org.gachon.checkmate.domain.member.entity; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public enum ProfileImageType { + + PROFILE_1("https://checkmate-dormitory-bucket.s3.ap-northeast-2.amazonaws.com/checkmate-profile1.png"), + PROFILE_2("https://example.com/profile2.jpg"), + PROFILE_3("https://example.com/profile3.jpg"); + + private final String imageUrl; +} diff --git a/src/main/java/org/gachon/checkmate/domain/member/entity/User.java b/src/main/java/org/gachon/checkmate/domain/member/entity/User.java index 48a8d22..40967b3 100644 --- a/src/main/java/org/gachon/checkmate/domain/member/entity/User.java +++ b/src/main/java/org/gachon/checkmate/domain/member/entity/User.java @@ -5,7 +5,7 @@ import org.gachon.checkmate.domain.checkList.entity.CheckList; import org.gachon.checkmate.domain.member.converter.GenderTypeConverter; import org.gachon.checkmate.domain.member.converter.MbtiTypeConverter; -import org.gachon.checkmate.domain.member.converter.RoomTypeConverter; +import org.gachon.checkmate.domain.post.converter.RoomTypeConverter; import org.gachon.checkmate.domain.post.entity.Post; import org.gachon.checkmate.domain.scrap.entity.Scrap; import org.gachon.checkmate.global.common.BaseTimeEntity; @@ -30,8 +30,6 @@ public class User extends BaseTimeEntity { private String profile; private String school; private String major; - @Convert(converter = RoomTypeConverter.class) - private RoomType roomType; @Convert(converter = MbtiTypeConverter.class) private MbtiType mbtiType; @Convert(converter = GenderTypeConverter.class) @@ -44,4 +42,21 @@ public class User extends BaseTimeEntity { @OneToMany(mappedBy = "user") @Builder.Default private List scrapList = new ArrayList<>(); + + public static User createUser(String email, String storedPassword, String name, String school, String major, MbtiType mbti, GenderType gender){ + return User.builder() + .email(email) + .password(storedPassword) + .name(name) + .profile(ProfileImageType.PROFILE_1.getImageUrl()) + .school(school) + .major(major) + .mbtiType(mbti) + .gender(gender) + .build(); + } + + public void setPassword(String newPassword) { + this.password = newPassword; + } } diff --git a/src/main/java/org/gachon/checkmate/domain/member/repository/UserRepository.java b/src/main/java/org/gachon/checkmate/domain/member/repository/UserRepository.java new file mode 100644 index 0000000..5d5013d --- /dev/null +++ b/src/main/java/org/gachon/checkmate/domain/member/repository/UserRepository.java @@ -0,0 +1,12 @@ +package org.gachon.checkmate.domain.member.repository; + +import org.gachon.checkmate.domain.member.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + + Optional findByEmail(String email); + boolean existsByEmail(String email); +} diff --git a/src/main/java/org/gachon/checkmate/domain/member/service/MemberService.java b/src/main/java/org/gachon/checkmate/domain/member/service/MemberService.java index 5d27983..037baeb 100644 --- a/src/main/java/org/gachon/checkmate/domain/member/service/MemberService.java +++ b/src/main/java/org/gachon/checkmate/domain/member/service/MemberService.java @@ -3,12 +3,25 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.gachon.checkmate.domain.member.dto.request.EmailPostRequestDto; +import org.gachon.checkmate.domain.member.dto.request.MemberSignInRequestDto; +import org.gachon.checkmate.domain.member.dto.request.MemberSignUpRequestDto; +import org.gachon.checkmate.domain.member.dto.request.PasswordResetRequestDto; import org.gachon.checkmate.domain.member.dto.response.EmailResponseDto; +import org.gachon.checkmate.domain.member.dto.response.MemberSignInResponseDto; +import org.gachon.checkmate.domain.member.dto.response.MemberSignUpResponseDto; +import org.gachon.checkmate.domain.member.entity.User; +import org.gachon.checkmate.domain.member.repository.UserRepository; import org.gachon.checkmate.global.config.auth.jwt.JwtProvider; import org.gachon.checkmate.global.config.mail.MailProvider; +import org.gachon.checkmate.global.error.exception.ConflictException; +import org.gachon.checkmate.global.error.exception.EntityNotFoundException; +import org.gachon.checkmate.global.error.exception.UnauthorizedException; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import static org.gachon.checkmate.global.error.ErrorCode.*; + @Slf4j @RequiredArgsConstructor @Transactional @@ -17,10 +30,80 @@ public class MemberService { private final JwtProvider jwtProvider; private final MailProvider mailProvider; + private final PasswordEncoder passwordEncoder; + private final UserRepository userRepository; public EmailResponseDto sendMail(EmailPostRequestDto emailPostRequestDto) { + checkDuplicateEmail(emailPostRequestDto.email()); String authNum = mailProvider.sendMail(emailPostRequestDto.email(), "email"); return new EmailResponseDto(authNum); } + public MemberSignUpResponseDto signUp(MemberSignUpRequestDto memberSignUpRequestDto) { + Long newMemberId = createMember(memberSignUpRequestDto); + String accessToken = issueNewAccessToken(newMemberId); + String refreshToken = issueNewRefreshToken(newMemberId); + return MemberSignUpResponseDto.of(newMemberId, memberSignUpRequestDto.name(), accessToken, refreshToken); + } + + public MemberSignInResponseDto signIn(MemberSignInRequestDto memberSignInRequestDto) { + User user = getUserFromEmail(memberSignInRequestDto.email()); + validatePassword(memberSignInRequestDto.password(), user.getPassword()); + String accessToken = issueNewAccessToken(user.getId()); + String refreshToken = issueNewRefreshToken(user.getId()); + return MemberSignInResponseDto.of(user.getId(), accessToken, refreshToken); + } + + public void setPassword(PasswordResetRequestDto passwordResetRequestDto){ + User user = getUserFromEmail(passwordResetRequestDto.email()); + user.setPassword(encodedPassword(passwordResetRequestDto.newPassword())); + } + + private void validatePassword(String enteredPassword, String storedPassword) { + if (!authenticatePassword(enteredPassword, storedPassword)) { + throw new UnauthorizedException(INVALID_PASSWORD); + } + } + + private void checkDuplicateEmail(String email) { + if (userRepository.existsByEmail(email)) { + throw new ConflictException(DUPLICATE_EMAIL); + } + } + + private String issueNewAccessToken(Long memberId) { + return jwtProvider.getIssueToken(memberId, true); + } + + private String issueNewRefreshToken(Long memberId) { + return jwtProvider.getIssueToken(memberId, false); + } + + private Long createMember(MemberSignUpRequestDto memberSignUpRequestDto) { + User newUser = User.createUser( + memberSignUpRequestDto.email(), + encodedPassword(memberSignUpRequestDto.password()), + memberSignUpRequestDto.name(), + memberSignUpRequestDto.school(), + memberSignUpRequestDto.major(), + memberSignUpRequestDto.mbtiType(), + memberSignUpRequestDto.genderType() + ); + return userRepository.save(newUser).getId(); + } + + private String encodedPassword(String rawPassword) { + return passwordEncoder.encode(rawPassword); + } + + private boolean authenticatePassword(String enteredPassword, String storedPassword) { + return passwordEncoder.matches(enteredPassword, storedPassword); + } + + private User getUserFromEmail(String email) { + return userRepository.findByEmail(email) + .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); + } + + } diff --git a/src/main/java/org/gachon/checkmate/domain/member/converter/RoomTypeConverter.java b/src/main/java/org/gachon/checkmate/domain/post/converter/RoomTypeConverter.java similarity index 71% rename from src/main/java/org/gachon/checkmate/domain/member/converter/RoomTypeConverter.java rename to src/main/java/org/gachon/checkmate/domain/post/converter/RoomTypeConverter.java index c0ea666..cf9c7ed 100644 --- a/src/main/java/org/gachon/checkmate/domain/member/converter/RoomTypeConverter.java +++ b/src/main/java/org/gachon/checkmate/domain/post/converter/RoomTypeConverter.java @@ -1,7 +1,7 @@ -package org.gachon.checkmate.domain.member.converter; +package org.gachon.checkmate.domain.post.converter; import jakarta.persistence.Converter; -import org.gachon.checkmate.domain.member.entity.RoomType; +import org.gachon.checkmate.domain.post.entity.RoomType; import org.gachon.checkmate.global.utils.AbstractEnumCodeAttributeConverter; @Converter diff --git a/src/main/java/org/gachon/checkmate/domain/post/entity/Post.java b/src/main/java/org/gachon/checkmate/domain/post/entity/Post.java index f932a4b..3533425 100644 --- a/src/main/java/org/gachon/checkmate/domain/post/entity/Post.java +++ b/src/main/java/org/gachon/checkmate/domain/post/entity/Post.java @@ -3,8 +3,7 @@ import jakarta.persistence.*; import lombok.*; import org.gachon.checkmate.domain.checkList.entity.PostCheckList; -import org.gachon.checkmate.domain.member.converter.RoomTypeConverter; -import org.gachon.checkmate.domain.member.entity.RoomType; +import org.gachon.checkmate.domain.post.converter.RoomTypeConverter; import org.gachon.checkmate.domain.member.entity.User; import org.gachon.checkmate.domain.post.converter.ImportantKeyTypeConverter; import org.gachon.checkmate.domain.post.converter.SimilarityKeyTypeConverter; diff --git a/src/main/java/org/gachon/checkmate/domain/member/entity/RoomType.java b/src/main/java/org/gachon/checkmate/domain/post/entity/RoomType.java similarity index 87% rename from src/main/java/org/gachon/checkmate/domain/member/entity/RoomType.java rename to src/main/java/org/gachon/checkmate/domain/post/entity/RoomType.java index 6e9763a..34c7c30 100644 --- a/src/main/java/org/gachon/checkmate/domain/member/entity/RoomType.java +++ b/src/main/java/org/gachon/checkmate/domain/post/entity/RoomType.java @@ -1,4 +1,4 @@ -package org.gachon.checkmate.domain.member.entity; +package org.gachon.checkmate.domain.post.entity; import lombok.AccessLevel; import lombok.AllArgsConstructor; diff --git a/src/main/java/org/gachon/checkmate/global/config/auth/SecurityConfig.java b/src/main/java/org/gachon/checkmate/global/config/auth/SecurityConfig.java index 8f3c23b..92ef638 100644 --- a/src/main/java/org/gachon/checkmate/global/config/auth/SecurityConfig.java +++ b/src/main/java/org/gachon/checkmate/global/config/auth/SecurityConfig.java @@ -12,6 +12,8 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -23,7 +25,7 @@ public class SecurityConfig { private final CorsConfig corsConfig; private final JwtProvider jwtProvider; - private static final String[] whiteList = {"/", "api/member/email"}; + private static final String[] whiteList = {"/", "api/member/email", "api/member/signup", "api/member/signin"}; @Bean public WebSecurityCustomizer webSecurityCustomizer() { @@ -47,4 +49,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .addFilterBefore(new ExceptionHandlerFilter(), JwtAuthenticationFilter.class) .build(); } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } } \ No newline at end of file diff --git a/src/main/java/org/gachon/checkmate/global/error/ErrorCode.java b/src/main/java/org/gachon/checkmate/global/error/ErrorCode.java index 983ca6a..61946f4 100644 --- a/src/main/java/org/gachon/checkmate/global/error/ErrorCode.java +++ b/src/main/java/org/gachon/checkmate/global/error/ErrorCode.java @@ -25,6 +25,7 @@ public enum ErrorCode { INVALID_REFRESH_TOKEN_VALUE(HttpStatus.UNAUTHORIZED, "리프레시 토큰의 값이 올바르지 않습니다."), EXPIRED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "리프레시 토큰이 만료되었습니다. 다시 로그인해 주세요."), NOT_MATCH_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "일치하지 않는 리프레시 토큰입니다."), + INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다."), /** * 403 Forbidden @@ -36,6 +37,7 @@ public enum ErrorCode { */ ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "엔티티를 찾을 수 없습니다."), CHECK_LIST_NOT_FOUND(HttpStatus.NOT_FOUND, "체크리스트를 찾을 수 없습니다."), + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당하는 유저를 찾을 수 없습니다."), /** * 405 Method Not Allowed @@ -46,6 +48,7 @@ public enum ErrorCode { * 409 Conflict */ CONFLICT(HttpStatus.CONFLICT, "이미 존재하는 리소스입니다."), + DUPLICATE_EMAIL(HttpStatus.CONFLICT, "이미 가입된 이메일입니다."), /** * 500 Internal Server Error