Skip to content

Commit

Permalink
Merge pull request #141 from side-peek/feat/#124-change-password
Browse files Browse the repository at this point in the history
비밀번호 수정 기능 구현
  • Loading branch information
uijin-j authored Mar 12, 2024
2 parents c17f46f + 2dae439 commit 2398c0b
Show file tree
Hide file tree
Showing 12 changed files with 276 additions and 38 deletions.
2 changes: 1 addition & 1 deletion sidepeek_backend_secret
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
Expand Down Expand Up @@ -90,7 +91,8 @@ public ResponseEntity<ErrorResponse> handleInvalidAuthenticationException(
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ErrorResponse> handleHttpMessageNotReadableException(
HttpMessageNotReadableException e) {
ErrorResponse errorResponse = ErrorResponse.of(HttpStatus.BAD_REQUEST, "API 요청 형식이 올바르지 않습니다.");
ErrorResponse errorResponse = ErrorResponse.of(HttpStatus.BAD_REQUEST,
"API 요청 형식이 올바르지 않습니다.");
log.debug(e.getMessage(), e.fillInStackTrace());

return ResponseEntity.status(HttpStatus.BAD_REQUEST)
Expand All @@ -107,6 +109,15 @@ public ResponseEntity<ErrorResponse> handleTokenValidationFailException(
.body(errorResponse);
}

@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorResponse> handleAccessDeniedException(AccessDeniedException e) {
ErrorResponse errorResponse = ErrorResponse.of(HttpStatus.FORBIDDEN, e.getMessage());
log.warn(e.getMessage(), e.fillInStackTrace());

return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(errorResponse);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
ErrorResponse errorResponse = ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ public static void validateNotBlank(String input, String message) {
Assert.isTrue(isNotBlank(input), message);
}

public static void validateNotNull(Object input, String message) {
Assert.notNull(input, message);
}

public static void validateBlank(String input, String message) {
Assert.isTrue(isBlank(input), message);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ public ResponseEntity<Void> updatePassword(
@PathVariable Long id,
@RequestBody @Valid UpdatePasswordRequest request
) {
// TODO: 비밀번호 변경 서비스 기능 구현
userService.updatePassword(loginId, id, request);

return ResponseEntity.noContent()
.build();
}
Expand Down
20 changes: 14 additions & 6 deletions src/main/java/sixgaezzang/sidepeek/users/domain/User.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package sixgaezzang.sidepeek.users.domain;

import static java.util.Objects.isNull;
import static sixgaezzang.sidepeek.common.util.SetUtils.isSetPossible;
import static sixgaezzang.sidepeek.common.util.validation.ValidationUtils.validateEmail;
import static sixgaezzang.sidepeek.common.util.validation.ValidationUtils.validateGithubUrl;
import static sixgaezzang.sidepeek.users.exception.message.UserErrorMessage.EMAIL_FORMAT_INVALID;
import static sixgaezzang.sidepeek.users.exception.message.UserErrorMessage.PASSWORD_NOT_REGISTERED;
import static sixgaezzang.sidepeek.users.exception.message.UserErrorMessage.USER_ALREADY_DELETED;
import static sixgaezzang.sidepeek.users.util.UserConstant.MAX_CAREER_LENGTH;
import static sixgaezzang.sidepeek.users.util.UserConstant.MAX_EMAIL_LENGTH;
Expand All @@ -26,14 +28,14 @@
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.SQLRestriction;
import org.springframework.security.crypto.password.PasswordEncoder;
import sixgaezzang.sidepeek.common.domain.BaseTimeEntity;
import sixgaezzang.sidepeek.common.util.validation.ValidationUtils;
import sixgaezzang.sidepeek.users.dto.request.UpdateUserProfileRequest;

@Entity
Expand Down Expand Up @@ -81,13 +83,14 @@ public class User extends BaseTimeEntity {
private LocalDateTime deletedAt;

@Builder
public User(String nickname, String email, Password password, String introduction,
String profileImageUrl, Job job, Career career, String githubUrl, String blogUrl) {
public User(String nickname, String email, String password, PasswordEncoder passwordEncoder,
String introduction,
String profileImageUrl, Job job, Career career, String githubUrl, String blogUrl) {
validateConstructorArguments(nickname, email);

this.nickname = nickname;
this.email = email;
this.password = password;
this.password = isNull(password) ? null : new Password(password, passwordEncoder);
this.introduction = introduction;
this.profileImageUrl = profileImageUrl;
this.job = job;
Expand All @@ -97,6 +100,8 @@ public User(String nickname, String email, Password password, String introductio
}

public boolean checkPassword(String rawPassword, PasswordEncoder passwordEncoder) {
ValidationUtils.validateNotNull(password, PASSWORD_NOT_REGISTERED);

return password != null && password.check(rawPassword, passwordEncoder);
}

Expand All @@ -113,8 +118,12 @@ public void update(UpdateUserProfileRequest request) {
setBlogUrl(request.blogUrl());
}

public void updatePassword(String newPassword, PasswordEncoder passwordEncoder) {
this.password = new Password(newPassword, passwordEncoder);
}

public void softDelete() { // TODO: 회원탈퇴할 때 언젠가는 쓰일 것 같아서 구현
if (Objects.isNull(this.deletedAt)) {
if (isNull(this.deletedAt)) {
this.deletedAt = LocalDateTime.now();
return;
}
Expand Down Expand Up @@ -199,5 +208,4 @@ private void setBlogUrl(String blogUrl) {
this.blogUrl = blogUrl;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public final class UserErrorMessage {
public static final String PASSWORD_FORMAT_INVALID = "비밀번호 형식이 올바르지 않습니다.";
public static final String PASSWORD_IS_NULL = "비밀번호를 입력해주세요.";
public static final String NEW_PASSWORD_IS_NULL = "새로운 비밀번호를 입력해주세요.";
public static final String PASSWORD_NOT_MATCH = "비밀번호가 일치하지 않습니다.";
public static final String PASSWORD_IS_SAME_AS_BEFORE = "기존 비밀번호와 동일한 비밀번호입니다.";
public static final String PASSWORD_NOT_REGISTERED = "소셜 로그인 회원입니다.";

// Introduction
public static final String INTRODUCTION_OVER_MAX_LENGTH =
Expand Down
50 changes: 42 additions & 8 deletions src/main/java/sixgaezzang/sidepeek/users/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import static sixgaezzang.sidepeek.users.exception.message.UserErrorMessage.EMAIL_FORMAT_INVALID;
import static sixgaezzang.sidepeek.users.exception.message.UserErrorMessage.NICKNAME_DUPLICATE;
import static sixgaezzang.sidepeek.users.exception.message.UserErrorMessage.NICKNAME_OVER_MAX_LENGTH;
import static sixgaezzang.sidepeek.users.exception.message.UserErrorMessage.PASSWORD_IS_SAME_AS_BEFORE;
import static sixgaezzang.sidepeek.users.exception.message.UserErrorMessage.PASSWORD_NOT_MATCH;
import static sixgaezzang.sidepeek.users.exception.message.UserErrorMessage.USER_NOT_EXISTING;
import static sixgaezzang.sidepeek.users.util.UserConstant.MAX_NICKNAME_LENGTH;
import static sixgaezzang.sidepeek.users.util.validation.UserValidator.validateLoginIdEqualsUserId;
Expand All @@ -16,12 +18,13 @@
import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import sixgaezzang.sidepeek.users.domain.Password;
import sixgaezzang.sidepeek.users.domain.User;
import sixgaezzang.sidepeek.users.dto.request.SignUpRequest;
import sixgaezzang.sidepeek.users.dto.request.UpdatePasswordRequest;
import sixgaezzang.sidepeek.users.dto.request.UpdateUserProfileRequest;
import sixgaezzang.sidepeek.users.dto.response.CheckDuplicateResponse;
import sixgaezzang.sidepeek.users.dto.response.UserProfileResponse;
Expand All @@ -43,11 +46,11 @@ public Long signUp(SignUpRequest request) {
verifyUniqueEmail(request.email());
verifyUniqueNickname(request.nickname());

Password encodedPassword = new Password(request.password(), passwordEncoder);
User user = User.builder()
.email(request.email())
.nickname(request.nickname())
.password(encodedPassword)
.password(request.password())
.passwordEncoder(passwordEncoder)
.build();

userRepository.save(user);
Expand Down Expand Up @@ -83,20 +86,19 @@ public CheckDuplicateResponse checkNicknameDuplicate(String nickname) {
public UserProfileResponse getProfileById(Long id) {
validateUserId(id);

User user = userRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException(USER_NOT_EXISTING));
User user = findUserById(id);

List<UserSkillSummary> techStacks = userSkillService.findAllByUser(user);

return UserProfileResponse.from(user, techStacks);
}

@Transactional
public UserProfileResponse updateProfile(Long loginId, Long id, UpdateUserProfileRequest request) {
public UserProfileResponse updateProfile(Long loginId, Long id,
UpdateUserProfileRequest request) {
validateLoginIdEqualsUserId(loginId, id);

User user = userRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException(USER_NOT_EXISTING));
User user = findUserById(id);

user.update(request);

Expand All @@ -105,6 +107,26 @@ public UserProfileResponse updateProfile(Long loginId, Long id, UpdateUserProfil
return UserProfileResponse.from(user, techStacks);
}

@Transactional
public void updatePassword(Long loginId, Long userId, UpdatePasswordRequest request) {
validateLoginIdEqualsUserId(loginId, userId);

String originalPassword = request.originalPassword();
String newPassword = request.password();

User user = findUserById(userId);

validateOriginalPassword(user, originalPassword);
validateNewPasswordIsDifferent(originalPassword, newPassword);

user.updatePassword(newPassword, passwordEncoder);
}

private User findUserById(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new EntityNotFoundException(USER_NOT_EXISTING));
}

private void verifyUniqueNickname(String nickname) {
if (userRepository.existsByNickname(nickname)) {
throw new EntityExistsException(NICKNAME_DUPLICATE);
Expand All @@ -117,4 +139,16 @@ private void verifyUniqueEmail(String email) {
}
}

private void validateOriginalPassword(User user, String originalPassword) {
if (!user.checkPassword(originalPassword, passwordEncoder)) {
throw new AccessDeniedException(PASSWORD_NOT_MATCH);
}
}

private void validateNewPasswordIsDifferent(String originalPassword, String newPassword) {
if (originalPassword.equals(newPassword)) {
throw new IllegalArgumentException(PASSWORD_IS_SAME_AS_BEFORE);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import sixgaezzang.sidepeek.auth.dto.response.LoginResponse;
import sixgaezzang.sidepeek.auth.jwt.JWTManager;
import sixgaezzang.sidepeek.common.exception.TokenValidationFailException;
import sixgaezzang.sidepeek.users.domain.Password;
import sixgaezzang.sidepeek.users.domain.User;
import sixgaezzang.sidepeek.users.dto.response.UserSummary;
import sixgaezzang.sidepeek.users.repository.UserRepository;
Expand Down Expand Up @@ -214,7 +213,8 @@ void setUp() {
private User createUser() {
User user = User.builder()
.email(email)
.password(new Password(password, passwordEncoder))
.password(password)
.passwordEncoder(passwordEncoder)
.nickname(nickname)
.build();

Expand Down
Loading

0 comments on commit 2398c0b

Please sign in to comment.