Skip to content
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package kgu.developers.api.user.application;

import kgu.developers.api.user.presentation.request.UserPasswordUpdateRequest;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

Expand Down Expand Up @@ -35,4 +36,9 @@ public void updateUser(UserUpdateRequest request) {
public UserDetailResponse getUserDetail() {
return userQueryService.getUserDetail();
}

public void updatePassword(UserPasswordUpdateRequest request) {
User user = userQueryService.me();
userCommandService.updatePassword(user, request.originalPassword(), request.newPassword());
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
package kgu.developers.api.user.presentation;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
Expand All @@ -11,9 +8,12 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import kgu.developers.api.user.presentation.request.UserCreateRequest;
import kgu.developers.api.user.presentation.request.UserPasswordUpdateRequest;
import kgu.developers.api.user.presentation.request.UserUpdateRequest;
import kgu.developers.api.user.presentation.response.UserPersistResponse;
import kgu.developers.domain.user.application.response.UserDetailResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;

@Tag(name = "User", description = "회원 API")
public interface UserController {
Expand Down Expand Up @@ -54,4 +54,16 @@ ResponseEntity<Void> updateUser(
required = true
) @Valid @RequestBody UserUpdateRequest request
);

@Operation(summary = "회원 비밀번호 수정 API", description = """
- Description : 이 API는 회원의 비밀번호를 수정 합니다.
- Assignee : 이신행
""")
@ApiResponse(responseCode = "204")
ResponseEntity<Void> updatePassword(
@Parameter(
description = "회원 비밀번호 수정 request 객체 입니다.",
required = true
) @Valid @RequestBody UserPasswordUpdateRequest request
);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package kgu.developers.api.user.presentation;

import static org.springframework.http.HttpStatus.CREATED;

import jakarta.validation.Valid;
import kgu.developers.api.user.application.UserFacade;
import kgu.developers.api.user.presentation.request.UserCreateRequest;
import kgu.developers.api.user.presentation.request.UserPasswordUpdateRequest;
import kgu.developers.api.user.presentation.request.UserUpdateRequest;
import kgu.developers.api.user.presentation.response.UserPersistResponse;
import kgu.developers.domain.user.application.response.UserDetailResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
Expand All @@ -10,18 +16,12 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import jakarta.validation.Valid;
import kgu.developers.api.user.application.UserFacade;
import kgu.developers.api.user.presentation.request.UserCreateRequest;
import kgu.developers.api.user.presentation.request.UserUpdateRequest;
import kgu.developers.api.user.presentation.response.UserPersistResponse;
import kgu.developers.domain.user.application.response.UserDetailResponse;
import lombok.RequiredArgsConstructor;
import static org.springframework.http.HttpStatus.CREATED;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/users")
public class UserControllerImpl implements UserController{
public class UserControllerImpl implements UserController {
private final UserFacade userFacade;

@Override
Expand All @@ -48,4 +48,13 @@ public ResponseEntity<Void> updateUser(
userFacade.updateUser(request);
return ResponseEntity.noContent().build();
}

@Override
@PatchMapping("/password")
public ResponseEntity<Void> updatePassword(
@Valid @RequestBody UserPasswordUpdateRequest request
) {
userFacade.updatePassword(request);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package kgu.developers.api.user.presentation.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Builder;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

@Builder
public record UserPasswordUpdateRequest(
@Schema(description = "원래 비밀번호", example = "password1234!", requiredMode = REQUIRED)
@Pattern(
regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*(),.?\":{}|<>])[A-Za-z\\d!@#$%^&*(),.?\":{}|<>]{8,15}$",
message = "비밀번호는 영문, 숫자, 특수문자를 포함하여 8~15자리여야 합니다."
)
@NotNull
String originalPassword,

@Schema(description = "새로운 비밀번호", example = "newpass1234!", requiredMode = REQUIRED)
@Pattern(
regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*(),.?\":{}|<>])[A-Za-z\\d!@#$%^&*(),.?\":{}|<>]{8,15}$",
message = "비밀번호는 영문, 숫자, 특수문자를 포함하여 8~15자리여야 합니다."
)
@NotNull
String newPassword
) {
}
60 changes: 46 additions & 14 deletions aics-api/src/testFixtures/java/user/application/UserFacadeTest.java
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
package user.application;

import static kgu.developers.domain.user.domain.Major.CSE;
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import kgu.developers.api.user.application.UserFacade;
import kgu.developers.api.user.presentation.request.UserCreateRequest;
import kgu.developers.api.user.presentation.request.UserPasswordUpdateRequest;
import kgu.developers.api.user.presentation.request.UserUpdateRequest;
import kgu.developers.api.user.presentation.response.UserPersistResponse;
import kgu.developers.domain.user.application.command.UserCommandService;
import kgu.developers.domain.user.application.query.UserQueryService;
import kgu.developers.domain.user.application.response.UserDetailResponse;
import kgu.developers.domain.user.domain.User;
import kgu.developers.domain.user.exception.InvalidPasswordException;
import mock.repository.FakeUserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import static kgu.developers.domain.user.domain.Major.CSE;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class UserFacadeTest {
private UserFacade userFacade;
Expand All @@ -29,14 +32,15 @@ public class UserFacadeTest {
public void init() {
FakeUserRepository fakeUserRepository = new FakeUserRepository();
UserQueryService userQueryService = new UserQueryService(fakeUserRepository);
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
userFacade = new UserFacade(
userQueryService,
new UserCommandService(new BCryptPasswordEncoder(), fakeUserRepository)
new UserCommandService(passwordEncoder, fakeUserRepository)
);

fakeUserRepository.save(User.builder()
.id("202411345")
.password("password1234")
.password(passwordEncoder.encode("password1234"))
.name("홍길동")
.email("test@kyonggi.ac.kr")
.phone("010-1234-5678")
Expand Down Expand Up @@ -102,4 +106,32 @@ public void getUserDetail_Success() {
assertEquals("010-1234-5678", result.phone());
assertEquals(CSE, result.major());
}

@Test
@DisplayName("updatePassword는 주어진 형식으로 변경하는 경우 예외를 던지지 않는다")
public void updatePassword_Success() {
// given
UserPasswordUpdateRequest request = UserPasswordUpdateRequest.builder()
.originalPassword("password1234")
.newPassword("newpass1234")
.build();

// then
assertDoesNotThrow(() -> userFacade.updatePassword(request));
}

@Test
@DisplayName("updatePassword는 원래 비밀번호를 잘못 입력할 경우 예외를 발생시킨다")
public void updatePassword_ThrowsException() {
// given
UserPasswordUpdateRequest request = UserPasswordUpdateRequest.builder()
.originalPassword("wrong1234")
.newPassword("newpass1234")
.build();

// when
// then
assertThatThrownBy(() -> userFacade.updatePassword(request))
.isInstanceOf(InvalidPasswordException.class);
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package kgu.developers.domain.user.application.command;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import kgu.developers.domain.user.domain.Major;
import kgu.developers.domain.user.domain.User;
import kgu.developers.domain.user.domain.UserRepository;
import kgu.developers.domain.user.exception.UserIdDuplicateException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
Expand All @@ -17,8 +16,7 @@ public class UserCommandService {

public String createUser(String userId, String password, String name, String email, String phone, Major major) {
validateDuplicateId(userId);
String encodedPassword = bCryptPasswordEncoder.encode(password);
User user = User.create(userId, encodedPassword, name, email, phone, major);
User user = User.create(userId, password, name, email, phone, major, bCryptPasswordEncoder);
return userRepository.save(user).getId();
}

Expand All @@ -31,4 +29,10 @@ private void validateDuplicateId(String id) {
if (userRepository.existsById(id))
throw new UserIdDuplicateException();
}

public void updatePassword(User user, String originalPassword, String newPassword) {
user.isNewPasswordMatching(newPassword, bCryptPasswordEncoder);
user.isPasswordMatching(originalPassword, bCryptPasswordEncoder);
user.updatePassword(newPassword, bCryptPasswordEncoder);
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,5 @@
package kgu.developers.domain.user.domain;

import static jakarta.persistence.CascadeType.ALL;
import static jakarta.persistence.EnumType.STRING;
import static jakarta.persistence.FetchType.LAZY;
import static kgu.developers.domain.user.domain.DeptCode.isValidDeptCode;
import static lombok.AccessLevel.PROTECTED;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Enumerated;
Expand All @@ -26,12 +10,28 @@
import kgu.developers.common.domain.BaseTimeEntity;
import kgu.developers.domain.post.domain.Post;
import kgu.developers.domain.user.exception.DeptCodeNotValidException;
import kgu.developers.domain.user.exception.DuplicatePasswordException;
import kgu.developers.domain.user.exception.EmailDomainNotValidException;
import kgu.developers.domain.user.exception.InvalidPasswordException;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import static jakarta.persistence.CascadeType.ALL;
import static jakarta.persistence.EnumType.STRING;
import static jakarta.persistence.FetchType.LAZY;
import static kgu.developers.domain.user.domain.DeptCode.isValidDeptCode;
import static lombok.AccessLevel.PROTECTED;

@Entity
@Getter
Expand Down Expand Up @@ -69,11 +69,12 @@ public class User extends BaseTimeEntity implements UserDetails {
@OneToMany(mappedBy = "author", cascade = ALL, fetch = LAZY)
List<Post> posts = new ArrayList<>();

public static User create(String id, String password, String name, String email, String phone, Major major) {
public static User create(String id, String password, String name, String email,
String phone, Major major, PasswordEncoder passwordEncoder) {
validateDept(id, email);
return User.builder()
.id(id)
.password(password)
.password(encodePassword(password, passwordEncoder))
.name(name)
.email(email)
.phone(phone)
Expand Down Expand Up @@ -133,4 +134,18 @@ public void isPasswordMatching(String rawPassword, PasswordEncoder passwordEncod
throw new InvalidPasswordException();
}
}

public void isNewPasswordMatching(String rawPassword, PasswordEncoder passwordEncoder) {
if (passwordEncoder.matches(rawPassword, this.password)) {
throw new DuplicatePasswordException();
}
}

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

private static String encodePassword(String password, PasswordEncoder passwordEncoder) {
return passwordEncoder.encode(password);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package kgu.developers.domain.user.exception;

import kgu.developers.common.exception.CustomException;

import static kgu.developers.domain.user.exception.UserDomainExceptionCode.DUPLICATE_PASSWORD;

public class DuplicatePasswordException extends CustomException {
public DuplicatePasswordException() {
super(DUPLICATE_PASSWORD);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public enum UserDomainExceptionCode implements ExceptionCode {
EMAIL_NOT_VALID(BAD_REQUEST, "학교 이메일 형식이 아닙니다."),
DEPT_CODE_NOT_VALID(BAD_REQUEST, "학번별 학과 코드가 일치하지 않습니다."),
INVALID_PASSWORD(BAD_REQUEST, "비밀번호가 일치하지 않습니다."),
DUPLICATE_PASSWORD(BAD_REQUEST, "기존 비밀번호와 동일한 비밀번호는 불가능합니다."),
USER_NOT_AUTHENTICATED(UNAUTHORIZED, "회원 인증에 실패하였습니다."),
USER_NOT_FOUND(NOT_FOUND, "해당 회원을 찾을 수 없습니다."),
USER_ID_DUPLICATED(CONFLICT, "이미 동일한 학번으로 가입이 되어있습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@
import mock.repository.FakeCommentRepository;
import mock.repository.FakePostRepository;
import mock.repository.FakeUserRepository;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

public class CommentCommandServiceTest {
private CommentCommandService commentCommandService;

private static final Long TARGET_COMMENT_ID = 1L;
private static final Long TEST_POST_ID = 1L;
private static final String TEST_USER_ID = "202411345";
private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder();

@BeforeEach
public void init() {
Expand All @@ -46,7 +49,7 @@ private static void saveTestUserAndPost(FakeUserRepository fakeUserRepository,
FakePostRepository fakePostRepository) {
fakeUserRepository.save(
User.create(TEST_USER_ID, "password1234", "홍길동", "honggildong@kyonggi.ac.kr",
"010-1234-5678", CSE)
"010-1234-5678", CSE, PASSWORD_ENCODER)
);
fakePostRepository.save(Post.create(
"SW 부트캠프 4기 교육생 모집", "SW전문인재양성사업단에서는 SW부트캠프 4기 교육생을 모집합니다.", NEWS,
Expand Down
Loading