From 46afb8e95808bca4380c5f2babfb9481185aec89 Mon Sep 17 00:00:00 2001 From: conagreen Date: Sun, 4 Feb 2024 22:15:01 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/controller/AuthController.java | 7 + .../page/server/domain/enumclass/Mail.java | 3 +- .../status/page/server/domain/model/User.java | 15 +- .../dto/request/auth/SignupRequestDto.java | 28 ++++ .../page/server/service/AuthService.java | 34 ++-- .../page/server/service/EmailService.java | 15 ++ src/main/resources/messages.properties | 2 + src/main/resources/templates/user_signup.html | 78 ++++++++++ .../server/controller/AuthControllerTest.java | 147 ++++++++++++++++++ src/test/resources/data/users.sql | 2 +- 10 files changed, 318 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/pickax/status/page/server/dto/request/auth/SignupRequestDto.java create mode 100644 src/main/resources/templates/user_signup.html diff --git a/src/main/java/com/pickax/status/page/server/controller/AuthController.java b/src/main/java/com/pickax/status/page/server/controller/AuthController.java index 8f9df16..d19af0d 100644 --- a/src/main/java/com/pickax/status/page/server/controller/AuthController.java +++ b/src/main/java/com/pickax/status/page/server/controller/AuthController.java @@ -2,6 +2,7 @@ import com.pickax.status.page.server.dto.request.auth.EmailAuthVerifyRequestDto; import com.pickax.status.page.server.service.AuthService; +import com.pickax.status.page.server.dto.request.auth.SignupRequestDto; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -32,6 +33,12 @@ public ResponseEntity verifyEmailAuthenticationCodeForSignup(@RequestBody return new ResponseEntity<>(HttpStatus.OK); } + @PostMapping("/signup") + public ResponseEntity signup(@RequestBody @Valid SignupRequestDto signupRequestDto) { + this.authService.signup(signupRequestDto); + return new ResponseEntity<>(HttpStatus.OK); + } + @PostMapping("/resign") public ResponseEntity resign(@RequestBody @Valid UserResignRequestDto userResignRequestDto) { authService.resign(userResignRequestDto); diff --git a/src/main/java/com/pickax/status/page/server/domain/enumclass/Mail.java b/src/main/java/com/pickax/status/page/server/domain/enumclass/Mail.java index 13c4389..cd07656 100644 --- a/src/main/java/com/pickax/status/page/server/domain/enumclass/Mail.java +++ b/src/main/java/com/pickax/status/page/server/domain/enumclass/Mail.java @@ -5,7 +5,8 @@ public enum Mail { COMPONENT_STATUS_NOTIFICATION("MAIL_TITLE_OF_COMPONENT_STATUS_NOTIFICATION", "COMPONENT_STATUS_NOTIFICATION"), SIGNUP_EMAIL_AUTHENTICATION_CODE("MAIL_TITLE_OF_SIGNUP_EMAIL_AUTHENTICATION_CODE", "SIGNUP_EMAIL_AUTHENTICATION_CODE"), - USER_RESIGN("MAIL_TITLE_OF_USER_RESIGN", "USER_RESIGN"); + USER_RESIGN("MAIL_TITLE_OF_USER_RESIGN", "USER_RESIGN"), + USER_SIGNUP("MAIL_TITLE_OF_USER_SIGNUP", "USER_SIGNUP"); @Getter private String subject; diff --git a/src/main/java/com/pickax/status/page/server/domain/model/User.java b/src/main/java/com/pickax/status/page/server/domain/model/User.java index c2320c6..bf068c9 100644 --- a/src/main/java/com/pickax/status/page/server/domain/model/User.java +++ b/src/main/java/com/pickax/status/page/server/domain/model/User.java @@ -27,12 +27,23 @@ public class User { @Enumerated(EnumType.STRING) private UserStatus status; - @Column(name = "sign_up_at", nullable = false) - private LocalDateTime signUpDate; + @Column(name = "signup_at", nullable = false) + private LocalDateTime signupDate; @Column(name = "withdrawal_at") private LocalDateTime withdrawalDate; + private User(String email, String password, UserStatus status, LocalDateTime signupDate) { + this.email = email; + this.password = password; + this.status = status; + this.signupDate = signupDate; + } + + public static User create(String email, String encodedPassword) { + return new User(email, encodedPassword, UserStatus.JOIN, LocalDateTime.now()); + } + public void delete() { email = ""; password = ""; diff --git a/src/main/java/com/pickax/status/page/server/dto/request/auth/SignupRequestDto.java b/src/main/java/com/pickax/status/page/server/dto/request/auth/SignupRequestDto.java new file mode 100644 index 0000000..a5114ff --- /dev/null +++ b/src/main/java/com/pickax/status/page/server/dto/request/auth/SignupRequestDto.java @@ -0,0 +1,28 @@ +package com.pickax.status.page.server.dto.request.auth; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +public class SignupRequestDto { + @Email + @NotBlank(message = "email 은(는) 공백일 수 없습니다.") + private String email; + + @NotBlank(message = "password 은(는) 공백일 수 없습니다.") + @Pattern(regexp = "^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d)(?=.*[!#$%&?])[A-Za-z\\d!#$%&?]{8,16}$") + private String password; + + @NotBlank(message = "code 은(는) 공백일 수 없습니다.") + private String code; + + public SignupRequestDto(String email, String password, String code) { + this.email = email; + this.password = password; + this.code = code; + } +} \ No newline at end of file diff --git a/src/main/java/com/pickax/status/page/server/service/AuthService.java b/src/main/java/com/pickax/status/page/server/service/AuthService.java index 4e83a25..6356744 100644 --- a/src/main/java/com/pickax/status/page/server/service/AuthService.java +++ b/src/main/java/com/pickax/status/page/server/service/AuthService.java @@ -4,6 +4,7 @@ import java.util.List; +import com.pickax.status.page.server.dto.request.auth.SignupRequestDto; import com.pickax.status.page.server.dto.request.auth.EmailAuthVerifyRequestDto; import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.core.Authentication; @@ -36,7 +37,6 @@ import java.time.LocalDateTime; import lombok.extern.slf4j.Slf4j; -import java.util.Optional; @Slf4j @Service @@ -55,18 +55,19 @@ public class AuthService { @Transactional public void sendEmailAuthenticationCodeForSignup(EmailAuthSendRequestDto emailAuthSendRequestDto) { String requestEmail = emailAuthSendRequestDto.getEmail(); - userRepository.getUser(requestEmail, UserStatus.JOIN).ifPresent(user -> { - throw new CustomException(ErrorCode.DUPLICATE_USER); - }); - + checkDuplicatedUser(requestEmail); cleanAllEmailAuthenticationByEmail(requestEmail); - String code = RandomStringUtils.randomNumeric(6); - emailAuthenticationRepository.save(EmailAuthentication.create(requestEmail, code, LocalDateTime.now().plusMinutes(10))); emailService.sendEmailAuthenticationCodeForSignup(requestEmail, code); } + private void checkDuplicatedUser(String email) { + userRepository.getUser(email, UserStatus.JOIN).ifPresent(user -> { + throw new CustomException(ErrorCode.DUPLICATE_USER); + }); + } + private void cleanAllEmailAuthenticationByEmail(String email) { List emailAuthentications = emailAuthenticationRepository.findAllByEmail(email); if (!emailAuthentications.isEmpty()) { @@ -108,10 +109,14 @@ private void cancelSiteAndDeactivateComponents(Site site) { @Transactional(readOnly = true) public void verifyEmailAuthenticationCodeForSignup(EmailAuthVerifyRequestDto emailAuthVerifyRequestDto) { - EmailAuthentication emailAuthentication = emailAuthenticationRepository.findFirst1ByEmail(emailAuthVerifyRequestDto.getEmail()) + verifyEmailAuthenticationCodeForSignup(emailAuthVerifyRequestDto.getEmail(), emailAuthVerifyRequestDto.getCode()); + } + + private void verifyEmailAuthenticationCodeForSignup(String email, String code) { + EmailAuthentication emailAuthentication = emailAuthenticationRepository.findFirst1ByEmail(email) .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_AUTHENTICATION_CODE)); - if (!emailAuthentication.verify(emailAuthVerifyRequestDto.getCode())) { + if (!emailAuthentication.verify(code)) { throw new CustomException(ErrorCode.INVALID_AUTHENTICATION_CODE); } @@ -134,4 +139,15 @@ private void verifyPassword(String inputPassword, String password) { throw new CustomException(ErrorCode.INVALID_PASSWORD); } } + + @Transactional + public void signup(SignupRequestDto signupRequestDto) { + String email = signupRequestDto.getEmail(); + checkDuplicatedUser(email); + verifyEmailAuthenticationCodeForSignup(email, signupRequestDto.getCode()); + userRepository.save(User.create(email, passwordEncoder.encode(signupRequestDto.getPassword()))); + + emailService.sendSignupEmail(email); + cleanAllEmailAuthenticationByEmail(email); + } } diff --git a/src/main/java/com/pickax/status/page/server/service/EmailService.java b/src/main/java/com/pickax/status/page/server/service/EmailService.java index e0f9d28..af6d6c4 100644 --- a/src/main/java/com/pickax/status/page/server/service/EmailService.java +++ b/src/main/java/com/pickax/status/page/server/service/EmailService.java @@ -63,4 +63,19 @@ private MailMessageContext createUserResignEmailContext(UserResignEvent userResi .to(userResignEvent.email()) .build(); } + + public void sendSignupEmail(String email) { + MailMessageContext messageContext = createSignupEmailContext(email); + emailSender.send(messageContext); + } + + private MailMessageContext createSignupEmailContext(String email) { + Context context = new Context(); + context.setVariable("receiverEmail", email); + return MailMessageContext.builder() + .context(context) + .mailType(Mail.USER_SIGNUP) + .to(email) + .build(); + } } diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 3d67ca9..23e1165 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -2,11 +2,13 @@ MAIL_TITLE_OF_COMPONENT_STATUS_NOTIFICATION=[QUACK_RUN] 컴포넌트 상태 변경 알림입니다. MAIL_TITLE_OF_SIGNUP_EMAIL_AUTHENTICATION_CODE=[QUACK_RUN] 회원 가입 인증 안내 메일입니다. MAIL_TITLE_OF_USER_RESIGN=[QUACK_RUN] 회원 탈퇴 안내 메일입니다. +MAIL_TITLE_OF_USER_SIGNUP=[QUACK_RUN] 회원 가입 안내 메일입니다. #MAILING TEMPLATE COMPONENT_STATUS_NOTIFICATION=component_status_notification SIGNUP_EMAIL_AUTHENTICATION_CODE=signup_email_authentication_code USER_RESIGN=user_resign +USER_SIGNUP=user_signup ## 400 BAD_REQUEST INVALID_INPUT_VALUE=Invalid input value diff --git a/src/main/resources/templates/user_signup.html b/src/main/resources/templates/user_signup.html new file mode 100644 index 0000000..5f3a581 --- /dev/null +++ b/src/main/resources/templates/user_signup.html @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + Quack run - User signup + + + + + + + + + + + + + + +
+
Quack Run - 회원가입 알림 + +  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­   +
+ + + + + +
+ + + + + + + + + + + +
+

+ 회원가입 알림

+
+
+

안녕하세요,

+

회원가입이 완료되었습니다.

+

+
+
+ 확인 +
+
+
+
+ + + diff --git a/src/test/java/com/pickax/status/page/server/controller/AuthControllerTest.java b/src/test/java/com/pickax/status/page/server/controller/AuthControllerTest.java index 4c0ee08..c843a24 100644 --- a/src/test/java/com/pickax/status/page/server/controller/AuthControllerTest.java +++ b/src/test/java/com/pickax/status/page/server/controller/AuthControllerTest.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.pickax.status.page.server.dto.request.auth.EmailAuthSendRequestDto; import com.pickax.status.page.server.dto.request.auth.EmailAuthVerifyRequestDto; +import com.pickax.status.page.server.dto.request.auth.SignupRequestDto; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -158,4 +159,150 @@ void verifyEmailAuthenticationCodeForSignupByInvalidCode() throws Exception { .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.customError").value(INVALID_AUTHENTICATION_CODE.name())); } + + @Test + @DisplayName("POST 회원가입 - 요청 성공 200 OK") + void signup() throws Exception { + // given + String url = "/auth/signup"; + SignupRequestDto requestDto = new SignupRequestDto("validuser1@ruu.kr", "User123!", "098677"); + + // when + mockMvc.perform(MockMvcRequestBuilders + .post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto)) + .accept(MediaType.APPLICATION_JSON) + ) + .andDo(print()) + + // then + .andExpect(status().isOk()); + } + + @Test + @DisplayName("POST 회원가입 - 존재하지 않는 email 인증 정보일 경우 404 NOT_FOUND") + void signupByNonExistentEmail() throws Exception { + // given + String url = "/auth/signup"; + SignupRequestDto requestDto = new SignupRequestDto("nonexistentuser@ruu.kr", "User123!", "098677"); + + // when + mockMvc.perform(MockMvcRequestBuilders + .post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto)) + .accept(MediaType.APPLICATION_JSON) + ) + .andDo(print()) + + // then + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.customError").value(NOT_FOUND_AUTHENTICATION_CODE.name())); + } + + @Test + @DisplayName("POST 회원가입 - 인증 번호의 유효 기간이 지난 경우 404 NOT_FOUND") + void signupOnExpiredDate() throws Exception { + // given + String url = "/auth/signup"; + SignupRequestDto requestDto = new SignupRequestDto("invaliduser2@ruu.kr", "User123!", "345334"); + + // when + mockMvc.perform(MockMvcRequestBuilders + .post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto)) + .accept(MediaType.APPLICATION_JSON) + ) + .andDo(print()) + + // then + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.customError").value(NOT_FOUND_AUTHENTICATION_CODE.name())); + } + + @Test + @DisplayName("POST 회원가입 - 비밀번호의 길이가 최소 길이보다 미만인 경우 400 BAD_REQUEST") + void signupByShorterPassword() throws Exception { + // given + String url = "/auth/signup"; + SignupRequestDto requestDto = new SignupRequestDto("validuser1@ruu.kr", "User12!", "123456"); + + // when + mockMvc.perform(MockMvcRequestBuilders + .post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto)) + .accept(MediaType.APPLICATION_JSON) + ) + .andDo(print()) + + // then + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.customError").value(INVALID_INPUT_VALUE.name())); + } + + @Test + @DisplayName("POST 회원가입 - 비밀번호의 길이가 최대 길이를 초과한 경우 400 BAD_REQUEST") + void signupByLongerPassword() throws Exception { + // given + String url = "/auth/signup"; + SignupRequestDto requestDto = new SignupRequestDto("validuser1@ruu.kr", "IAmUserHaha1234!#", "123456"); + + // when + mockMvc.perform(MockMvcRequestBuilders + .post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto)) + .accept(MediaType.APPLICATION_JSON) + ) + .andDo(print()) + + // then + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.customError").value(INVALID_INPUT_VALUE.name())); + } + + @Test + @DisplayName("POST 회원가입 - 비밀번호에 필수 사항이 누락된 경우 400 BAD_REQUEST") + void signupByPasswordMissingRequirements() throws Exception { + // given + String url = "/auth/signup"; + SignupRequestDto requestDto = new SignupRequestDto("validuser1@ruu.kr", "User1234", "123456"); + + // when + mockMvc.perform(MockMvcRequestBuilders + .post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto)) + .accept(MediaType.APPLICATION_JSON) + ) + .andDo(print()) + + // then + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.customError").value(INVALID_INPUT_VALUE.name())); + } + + @Test + @DisplayName("POST 회원가입 - 비밀번호에 허용되지 않은 기호가 포함된 경우 400 BAD_REQUEST") + void signupByPasswordContainingDisallowedSymbols() throws Exception { + // given + String url = "/auth/signup"; + SignupRequestDto requestDto = new SignupRequestDto("validuser1@ruu.kr", "User1234*", "123456"); + + // when + mockMvc.perform(MockMvcRequestBuilders + .post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto)) + .accept(MediaType.APPLICATION_JSON) + ) + .andDo(print()) + + // then + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.customError").value(INVALID_INPUT_VALUE.name())); + } } \ No newline at end of file diff --git a/src/test/resources/data/users.sql b/src/test/resources/data/users.sql index e2ec54e..ddfab3f 100644 --- a/src/test/resources/data/users.sql +++ b/src/test/resources/data/users.sql @@ -4,6 +4,6 @@ truncate table `users`; set FOREIGN_KEY_CHECKS = 1; -insert into users(id, email, password, status, sign_up_at) +insert into users(id, email, password, status, signup_at) values (1, 'user1@ruu.kr', 'qwerR1234!', 'JOIN', '2024-01-01 00:00:00'), (2, 'user2@ruu.kr', 'qwerR1234!', 'JOIN', '2024-01-01 00:00:10');