diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/AuthController.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/AuthController.java index f34509e..b8d5307 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/AuthController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/AuthController.java @@ -124,4 +124,36 @@ public DefaultResponse checkVerificationCode(@Valid @RequestBody EmailRe emailCommandService.checkEmail(request); return DefaultResponse.noContent(); } + + @Operation(summary = "비밀번호 찾기 API", description = "이메일 인증 이후 이메일과 새로운 비밀번호로 비밀번호 변경") + @ApiResponses({ + @ApiResponse(responseCode = "204", description = "비밀번호 변경 성공"), + @ApiResponse( + responseCode = "400", + description = """ + 다음과 같은 이유로 실패할 수 있습니다: + - AUTH400_2: 소셜 로그인으로 가입된 사용자입니다. + - MEMBER400_1: 이전 비밀번호와 동일합니다. + """ + ), + @ApiResponse( + responseCode = "401", + description = """ + 다음과 같은 이유로 실패할 수 있습니다: + - EMAIL401_2: 비밀번호 재설정에 이메일 인증을 하지 않았습니다. + """ + ), + @ApiResponse( + responseCode = "404", + description = """ + 다음과 같은 이유로 실패할 수 있습니다: + - MEMBER404_1: 사용자를 찾지 못했습니다. + """ + ), + }) + @PostMapping("/passwords") + public DefaultResponse findPassword(@RequestBody AuthRequestDTO.FindPassword request) { + authCommandService.findPassword(request); + return DefaultResponse.noContent(); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/dto/request/AuthRequestDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/dto/request/AuthRequestDTO.java index 858b1c3..b1a21d7 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/dto/request/AuthRequestDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/dto/request/AuthRequestDTO.java @@ -27,4 +27,11 @@ public record SignUp( ) { } + + public record FindPassword( + String email, + String newPassword + ) { + + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/AuthCommandService.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/AuthCommandService.java index 165f06c..5150223 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/AuthCommandService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/AuthCommandService.java @@ -2,10 +2,12 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.bind.annotation.RequestBody; import org.withtime.be.withtimebe.domain.auth.dto.request.AuthRequestDTO; public interface AuthCommandService { void signUp(AuthRequestDTO.SignUp request); void reissueToken(HttpServletRequest request, HttpServletResponse response); void logout(HttpServletRequest request, HttpServletResponse response); + void findPassword(@RequestBody AuthRequestDTO.FindPassword request); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/AuthCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/AuthCommandServiceImpl.java index aaa3057..d06c43e 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/AuthCommandServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/AuthCommandServiceImpl.java @@ -15,6 +15,7 @@ import org.withtime.be.withtimebe.domain.member.entity.Social; import org.withtime.be.withtimebe.domain.member.repository.MemberRepository; import org.withtime.be.withtimebe.domain.member.repository.SocialRepository; +import org.withtime.be.withtimebe.domain.member.service.command.MemberCommandService; import org.withtime.be.withtimebe.global.error.code.*; import org.withtime.be.withtimebe.global.error.exception.*; import org.withtime.be.withtimebe.global.security.constants.AuthenticationConstants; @@ -29,6 +30,7 @@ public class AuthCommandServiceImpl implements AuthCommandService { private final PasswordEncoder passwordEncoder; private final MemberRepository memberRepository; private final SocialRepository socialRepository; + private final MemberCommandService memberCommandService; private final TokenCommandService tokenCommandService; private final TokenStorageCommandService tokenStorageCommandService; private final TokenQueryService tokenQueryService; @@ -39,7 +41,7 @@ public class AuthCommandServiceImpl implements AuthCommandService { public void signUp(AuthRequestDTO.SignUp request) { validateSignUp(request); - Member member = memberRepository.save(AuthConverter.toLocalMember(request.email(), request.username(), request.socialId() != null ? passwordEncoder.encode(request.password()) : null, request.phoneNumber(), request.gender(), request.birth())); + Member member = memberRepository.save(AuthConverter.toLocalMember(request.email(), request.username(), request.socialId() != null ? null : passwordEncoder.encode(request.password()), request.phoneNumber(), request.gender(), request.birth())); if (request.socialId() != null) { Social social = socialRepository.findById(request.socialId()).orElseThrow(() -> new SocialException(SocialErrorCode.NOT_FOUND_SOCIAL)); @@ -82,6 +84,14 @@ public void logout(HttpServletRequest request, HttpServletResponse response) { CookieUtil.deleteCookie(request, response, AuthenticationConstants.REFRESH_TOKEN_NAME); } + @Override + public void findPassword(AuthRequestDTO.FindPassword request) { + if (!emailVerificationCodeStorageQueryService.isVerified(request.email())) { + throw new EmailException(EmailErrorCode.UNVERIFIED_EMAIL); + } + memberCommandService.changePassword(request.email(), request.newPassword()); + } + private void reissueTokenInCookie(HttpServletRequest request, HttpServletResponse response, Long userId) { Member member = memberRepository.findById(userId).orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/controller/MemberController.java b/src/main/java/org/withtime/be/withtimebe/domain/member/controller/MemberController.java new file mode 100644 index 0000000..4220e46 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/controller/MemberController.java @@ -0,0 +1,78 @@ +package org.withtime.be.withtimebe.domain.member.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.namul.api.payload.response.DefaultResponse; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.withtime.be.withtimebe.domain.member.converter.MemberConverter; +import org.withtime.be.withtimebe.domain.member.dto.MemberRequestDTO; +import org.withtime.be.withtimebe.domain.member.dto.MemberResponseDTO; +import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.domain.member.service.command.MemberCommandService; +import org.withtime.be.withtimebe.global.security.annotation.AuthenticatedMember; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/members") +@Tag(name = "사용자 API") +public class MemberController { + + private final MemberCommandService memberCommandService; + + @Operation(summary = "비밀번호 변경 API", description = "현재 비밀번호가 맞으면 새로운 비밀번호로 변경") + @ApiResponses({ + @ApiResponse(responseCode = "204", description = "비밀번호 변경 성공"), + @ApiResponse( + responseCode = "400", + description = """ + 다음과 같은 이유로 실패할 수 있습니다: + - AUTH400_2: 소셜 로그인으로 가입된 사용자입니다. + - MEMBER400_1: 이전 비밀번호와 동일합니다. + """ + ), + @ApiResponse( + responseCode = "401", + description = """ + 다음과 같은 이유로 실패할 수 있습니다: + - AUTH401_2: 현재 비밀번호가 맞지 않습니다. + """ + ), + @ApiResponse( + responseCode = "404", + description = """ + 다음과 같은 이유로 실패할 수 있습니다: + - MEMBER404_1: 사용자를 찾지 못했습니다. + """ + ), + }) + @PatchMapping("/passwords") + public DefaultResponse changePassword(@AuthenticatedMember Member member, + @RequestBody MemberRequestDTO.ChangePassword request) { + memberCommandService.changePassword(member, request); + return DefaultResponse.noContent(); + } + + @Operation(summary = "사용자 정보 변경 API", description = "사용자 정보 변경, 사용자를 쿠키로 인식하여 정보를 변경") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "정보 변경 성공"), + @ApiResponse( + responseCode = "404", + description = """ + 다음과 같은 이유로 실패할 수 있습니다: + - MEMBER404_1: 사용자를 찾지 못했습니다. + """ + ) + }) + @PatchMapping("/infos") + public DefaultResponse changeInfo(@AuthenticatedMember Member member, + @RequestBody MemberRequestDTO.ChangeInfo request) { + Member updatedMember = memberCommandService.changeInfo(member.getId(), request); + return DefaultResponse.ok(MemberConverter.toChangeInfo(updatedMember)); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/converter/MemberConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/member/converter/MemberConverter.java new file mode 100644 index 0000000..02e018f --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/converter/MemberConverter.java @@ -0,0 +1,13 @@ +package org.withtime.be.withtimebe.domain.member.converter; + +import org.withtime.be.withtimebe.domain.member.dto.MemberResponseDTO; +import org.withtime.be.withtimebe.domain.member.entity.Member; + +public class MemberConverter { + + public static MemberResponseDTO.ChangeInfo toChangeInfo(Member member) { + return MemberResponseDTO.ChangeInfo.builder() + .username(member.getUsername()) + .build(); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/dto/MemberRequestDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/member/dto/MemberRequestDTO.java new file mode 100644 index 0000000..05471d7 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/dto/MemberRequestDTO.java @@ -0,0 +1,17 @@ +package org.withtime.be.withtimebe.domain.member.dto; + +public record MemberRequestDTO() { + + public record ChangePassword( + String nowPassword, + String newPassword + ) { + + } + + public record ChangeInfo( + String username + ) { + + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/dto/MemberResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/member/dto/MemberResponseDTO.java new file mode 100644 index 0000000..c3225ea --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/dto/MemberResponseDTO.java @@ -0,0 +1,12 @@ +package org.withtime.be.withtimebe.domain.member.dto; + +import lombok.Builder; + +public record MemberResponseDTO() { + @Builder + public record ChangeInfo( + String username + ) { + + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java index c97e6a7..d838788 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java @@ -42,9 +42,6 @@ public class Member extends BaseEntity { @Column(name = "password") private String password; - @Column(name = "nickname") - private String nickname; - @Column(name = "gender") @Enumerated(EnumType.STRING) private Gender gender; @@ -58,4 +55,12 @@ public class Member extends BaseEntity { @Enumerated(EnumType.STRING) @Column(name = "role", nullable = false) private Role role; + + public void changeUsername(String newUsername) { + this.username= newUsername; + } + + public void changePassword(String encodedPassword) { + this.password = encodedPassword; + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandService.java b/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandService.java new file mode 100644 index 0000000..eed5c12 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandService.java @@ -0,0 +1,10 @@ +package org.withtime.be.withtimebe.domain.member.service.command; + +import org.withtime.be.withtimebe.domain.member.dto.MemberRequestDTO; +import org.withtime.be.withtimebe.domain.member.entity.Member; + +public interface MemberCommandService { + void changePassword(Member member, MemberRequestDTO.ChangePassword request); + void changePassword(String email, String password); + Member changeInfo(Long memberId, MemberRequestDTO.ChangeInfo request); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandServiceImpl.java new file mode 100644 index 0000000..48f16ec --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandServiceImpl.java @@ -0,0 +1,58 @@ +package org.withtime.be.withtimebe.domain.member.service.command; + +import lombok.RequiredArgsConstructor; +import org.namul.api.payload.error.exception.ServerApplicationException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.withtime.be.withtimebe.domain.member.dto.MemberRequestDTO; +import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.domain.member.repository.MemberRepository; +import org.withtime.be.withtimebe.global.error.code.AuthErrorCode; +import org.withtime.be.withtimebe.global.error.code.MemberErrorCode; +import org.withtime.be.withtimebe.global.error.exception.AuthException; +import org.withtime.be.withtimebe.global.error.exception.MemberException; + +@Service +@RequiredArgsConstructor +@Transactional +public class MemberCommandServiceImpl implements MemberCommandService { + + private final PasswordEncoder passwordEncoder; + private final MemberRepository memberRepository; + + + @Override + public void changePassword(Member member, MemberRequestDTO.ChangePassword request) { + if (member.getPassword() != null && !passwordEncoder.matches(request.nowPassword(), member.getPassword())) { + throw new AuthException(AuthErrorCode.INCORRECT_PASSWORD); + } + this.changePassword(member.getEmail(), request.newPassword()); + } + + @Override + public void changePassword(String email, String password) { + Member member = memberRepository.findByEmail(email).orElseThrow(() -> + new MemberException(MemberErrorCode.NOT_FOUND)); + validateChangePassword(member, password); + member.changePassword(passwordEncoder.encode(password)); + } + + @Override + public Member changeInfo(Long memberId, MemberRequestDTO.ChangeInfo request) { + Member member = memberRepository.findById(memberId).orElseThrow(() -> + new MemberException(MemberErrorCode.NOT_FOUND)); + member.changeUsername(request.username()); + return member; + } + + public void validateChangePassword(Member member, String password) throws ServerApplicationException { + String memberPassword = member.getPassword(); + if (memberPassword == null) { + throw new AuthException(AuthErrorCode.ONLY_AVAILABLE_SOCIAL); + } + else if (passwordEncoder.matches(password, memberPassword)) { + throw new MemberException(MemberErrorCode.SAME_PASSWORD); + } + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/service/MemberQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/member/service/query/MemberQueryService.java similarity index 67% rename from src/main/java/org/withtime/be/withtimebe/domain/member/service/MemberQueryService.java rename to src/main/java/org/withtime/be/withtimebe/domain/member/service/query/MemberQueryService.java index d5b99a8..8c9a21f 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/service/MemberQueryService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/service/query/MemberQueryService.java @@ -1,4 +1,4 @@ -package org.withtime.be.withtimebe.domain.member.service; +package org.withtime.be.withtimebe.domain.member.service.query; import org.withtime.be.withtimebe.domain.member.entity.Member; diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/service/MemberQueryServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/member/service/query/MemberQueryServiceImpl.java similarity index 91% rename from src/main/java/org/withtime/be/withtimebe/domain/member/service/MemberQueryServiceImpl.java rename to src/main/java/org/withtime/be/withtimebe/domain/member/service/query/MemberQueryServiceImpl.java index b853a3d..b14f0fe 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/service/MemberQueryServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/service/query/MemberQueryServiceImpl.java @@ -1,4 +1,4 @@ -package org.withtime.be.withtimebe.domain.member.service; +package org.withtime.be.withtimebe.domain.member.service.query; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/org/withtime/be/withtimebe/global/error/code/AuthErrorCode.java b/src/main/java/org/withtime/be/withtimebe/global/error/code/AuthErrorCode.java index dc0d7e9..b67e846 100644 --- a/src/main/java/org/withtime/be/withtimebe/global/error/code/AuthErrorCode.java +++ b/src/main/java/org/withtime/be/withtimebe/global/error/code/AuthErrorCode.java @@ -11,7 +11,8 @@ public enum AuthErrorCode implements BaseErrorCode { NOT_FOUND_LOGIN_MEMBER(HttpStatus.NOT_FOUND, "AUTH404_1", "해당 이메일을 찾을 수 없습니다."), FAIL_AUTH_LOGIN(HttpStatus.UNAUTHORIZED, "AUTH401_1", "일반 로그인에 실패했습니다."), ALREADY_EXIST_EMAIL(HttpStatus.BAD_REQUEST, "AUTH400_1", "이미 존재하는 이메일입니다."), - ONLY_AVAILABLE_SOCIAL(HttpStatus.BAD_REQUEST, "AUTH400_2", "소셜 로그인만 가능합니다.") + ONLY_AVAILABLE_SOCIAL(HttpStatus.BAD_REQUEST, "AUTH400_2", "소셜 로그인만 가능합니다."), + INCORRECT_PASSWORD(HttpStatus.UNAUTHORIZED, "AUTH401_2", "비밀번호가 맞지 않습니다.") ; private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/org/withtime/be/withtimebe/global/error/code/MemberErrorCode.java b/src/main/java/org/withtime/be/withtimebe/global/error/code/MemberErrorCode.java index 587e3be..7504456 100644 --- a/src/main/java/org/withtime/be/withtimebe/global/error/code/MemberErrorCode.java +++ b/src/main/java/org/withtime/be/withtimebe/global/error/code/MemberErrorCode.java @@ -9,6 +9,7 @@ public enum MemberErrorCode implements BaseErrorCode { NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404_1", "사용자를 찾지 못했습니다."), + SAME_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER400_1", "이전 비밀번호와 동일합니다."), ; private final HttpStatus httpStatus; diff --git a/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java b/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java index fec718b..7d0ff94 100644 --- a/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java +++ b/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java @@ -28,7 +28,7 @@ import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.withtime.be.withtimebe.domain.auth.service.query.TokenStorageQueryService; -import org.withtime.be.withtimebe.domain.member.service.MemberQueryService; +import org.withtime.be.withtimebe.domain.member.service.query.MemberQueryService; import org.withtime.be.withtimebe.global.security.filter.JsonLoginFilter; import org.withtime.be.withtimebe.global.security.filter.JwtFilter; import org.withtime.be.withtimebe.global.security.handler.CustomAccessDeniedHandler; diff --git a/src/main/java/org/withtime/be/withtimebe/global/security/filter/JwtFilter.java b/src/main/java/org/withtime/be/withtimebe/global/security/filter/JwtFilter.java index fd23f49..5792111 100644 --- a/src/main/java/org/withtime/be/withtimebe/global/security/filter/JwtFilter.java +++ b/src/main/java/org/withtime/be/withtimebe/global/security/filter/JwtFilter.java @@ -20,7 +20,7 @@ import org.springframework.web.filter.OncePerRequestFilter; import org.withtime.be.withtimebe.domain.auth.service.query.TokenStorageQueryService; import org.withtime.be.withtimebe.domain.member.entity.Member; -import org.withtime.be.withtimebe.domain.member.service.MemberQueryService; +import org.withtime.be.withtimebe.domain.member.service.query.MemberQueryService; import org.withtime.be.withtimebe.global.security.constants.AuthenticationConstants; import org.withtime.be.withtimebe.global.security.domain.CustomUserDetails; import org.withtime.be.withtimebe.global.util.CookieUtil;