From 9de105e624bd9a17084f0f323c793c55b60a6630 Mon Sep 17 00:00:00 2001 From: duehee <149302959+duehee@users.noreply.github.com> Date: Thu, 4 Apr 2024 13:03:54 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat=20:=20=EC=88=98=EC=A0=95=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=201=EC=B0=A8=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/user/service/StudentService.java | 14 ++++++++++++++ .../koin/domain/user/service/UserService.java | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java b/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java index 5457844a2..6a9f61c15 100644 --- a/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java +++ b/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java @@ -3,11 +3,13 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import in.koreatech.koin.domain.user.dto.FindPasswordRequest; import in.koreatech.koin.domain.user.dto.StudentResponse; import in.koreatech.koin.domain.user.dto.StudentUpdateRequest; import in.koreatech.koin.domain.user.dto.StudentUpdateResponse; import in.koreatech.koin.domain.user.exception.DuplicationNicknameException; import in.koreatech.koin.domain.user.exception.StudentDepartmentNotValidException; +import in.koreatech.koin.domain.user.exception.UserNotFoundException; import in.koreatech.koin.domain.user.model.Student; import in.koreatech.koin.domain.user.model.StudentDepartment; import in.koreatech.koin.domain.user.model.User; @@ -54,4 +56,16 @@ public void checkDepartmentValid(String department) { throw StudentDepartmentNotValidException.withDetail("학부(학과) : " + department); } } + + @Transactional + public void sendResetPasswordEmail(FindPasswordRequest request) { + } + + public void changePasswordConfig(FindPasswordRequest request, String host) { + User user = userRepository.getByEmail(request.email()); + if (user == null) { + throw UserNotFoundException.withDetail("email : " + request.email()); + } + + } } diff --git a/src/main/java/in/koreatech/koin/domain/user/service/UserService.java b/src/main/java/in/koreatech/koin/domain/user/service/UserService.java index 69872a2ad..3a7dc9676 100644 --- a/src/main/java/in/koreatech/koin/domain/user/service/UserService.java +++ b/src/main/java/in/koreatech/koin/domain/user/service/UserService.java @@ -92,4 +92,8 @@ public void checkUserNickname(NicknameCheckExistsRequest request) { throw DuplicationEmailException.withDetail("nickname: " + request.nickname()); }); } + + public void sendResetTokenByEmailForFindPassword(String resetToken, String contextPath, String email) { + + } } From 11adf766fb253ab3a6225dfe47889a62264a3bb1 Mon Sep 17 00:00:00 2001 From: duehee <149302959+duehee@users.noreply.github.com> Date: Fri, 5 Apr 2024 00:14:08 +0900 Subject: [PATCH 02/12] =?UTF-8?q?feat=20:=20mailForm=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20=EB=B0=8F=20DTO,=20mail=20html=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/dto/FindPasswordRequest.java | 19 ++++++ .../domain/user/service/StudentService.java | 13 ++-- .../email/form/StudentPasswordChangeData.java | 34 ++++++++++ ...nt_change_password_certificate_button.html | 62 +++++++++++++++++++ 4 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 src/main/java/in/koreatech/koin/domain/user/dto/FindPasswordRequest.java create mode 100644 src/main/java/in/koreatech/koin/global/domain/email/form/StudentPasswordChangeData.java create mode 100644 src/main/resources/mail/student_change_password_certificate_button.html diff --git a/src/main/java/in/koreatech/koin/domain/user/dto/FindPasswordRequest.java b/src/main/java/in/koreatech/koin/domain/user/dto/FindPasswordRequest.java new file mode 100644 index 000000000..a71d88aee --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/user/dto/FindPasswordRequest.java @@ -0,0 +1,19 @@ +package in.koreatech.koin.domain.user.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; + +public record FindPasswordRequest( + @Email(message = "아우누리 계정 형식이 아닙니다. ${validatedValue}") + @NotNull(message = "이메일은 비어있을 수 없습니다.") + @Schema(description = "이메일 주소", requiredMode = REQUIRED, example = "asdf@koreatech.ac.kr") + @JsonProperty(value = "address") + String email +) { + +} diff --git a/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java b/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java index 6a9f61c15..adf80fd52 100644 --- a/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java +++ b/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java @@ -16,6 +16,8 @@ import in.koreatech.koin.domain.user.model.UserGender; import in.koreatech.koin.domain.user.repository.StudentRepository; import in.koreatech.koin.domain.user.repository.UserRepository; +import in.koreatech.koin.global.domain.email.form.StudentPasswordChangeData; +import in.koreatech.koin.global.domain.email.service.MailService; import lombok.RequiredArgsConstructor; @Service @@ -25,6 +27,7 @@ public class StudentService { private final StudentRepository studentRepository; private final UserRepository userRepository; + private final MailService mailService; public StudentResponse getStudent(Long userId) { Student student = studentRepository.getById(userId); @@ -57,15 +60,11 @@ public void checkDepartmentValid(String department) { } } - @Transactional - public void sendResetPasswordEmail(FindPasswordRequest request) { - } - - public void changePasswordConfig(FindPasswordRequest request, String host) { + public void sendFindPasswordMail(FindPasswordRequest request, String serverURL) { User user = userRepository.getByEmail(request.email()); if (user == null) { - throw UserNotFoundException.withDetail("email : " + request.email()); + throw UserNotFoundException.withDetail("존재하지 않는 이메일입니다." + request.email()); } - + mailService.sendMail(request.email(), new StudentPasswordChangeData(serverURL, user.getResetToken())); } } diff --git a/src/main/java/in/koreatech/koin/global/domain/email/form/StudentPasswordChangeData.java b/src/main/java/in/koreatech/koin/global/domain/email/form/StudentPasswordChangeData.java new file mode 100644 index 000000000..3220ff211 --- /dev/null +++ b/src/main/java/in/koreatech/koin/global/domain/email/form/StudentPasswordChangeData.java @@ -0,0 +1,34 @@ +package in.koreatech.koin.global.domain.email.form; + +import java.util.Map; + +public class StudentPasswordChangeData implements MailFormData { + private static final String SUBJECT = "학생 비밀번호 변경"; + private static final String PATH = "student_change_password_certificate_button"; + + private final String contextPath; + private final String resetToken; + + public StudentPasswordChangeData(String contextPath, String resetToken) { + this.contextPath = contextPath; + this.resetToken = resetToken; + } + + @Override + public Map getContent() { + return Map.of( + "contextPath", contextPath, + "resetToken", resetToken + ); + } + + @Override + public String getSubject() { + return SUBJECT; + } + + @Override + public String getFilePath() { + return PATH; + } +} diff --git a/src/main/resources/mail/student_change_password_certificate_button.html b/src/main/resources/mail/student_change_password_certificate_button.html new file mode 100644 index 000000000..942b054e9 --- /dev/null +++ b/src/main/resources/mail/student_change_password_certificate_button.html @@ -0,0 +1,62 @@ + + + + + 코인 이메일 인증 폼 + + + + + + + + + + + + + + +
+ Creating Email Magic +
+ + + + + + + +
+ 계정 비밀번호 변경 인증 +
+ 안녕하세요.

+ 한국기술교육대학교 커뮤니티 코인 운영팀입니다.
+
+ 비밀번호를 변경하려면 아래 버튼을 클릭하세요.

+ + 비밀번호 변경하기 +
+
+ + + + + +
+ Copyright BCSD Lab All rights reserved. +
+
+ + From 497ecfac47b9980c6f15b30748337118656f10af Mon Sep 17 00:00:00 2001 From: daheeParkk Date: Fri, 5 Apr 2024 02:41:05 +0900 Subject: [PATCH 03/12] =?UTF-8?q?feat:=20controller=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 17 ++++++++-- .../domain/user/service/StudentService.java | 2 +- .../koin/domain/user/service/UserService.java | 4 --- .../koreatech/koin/global/host/ServerURL.java | 16 ++++++++++ .../host/ServerURLArgumentResolver.java | 29 +++++++++++++++++ .../koin/global/host/ServerURLContext.java | 19 +++++++++++ .../global/host/ServerURLInterceptor.java | 32 +++++++++++++++++++ 7 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 src/main/java/in/koreatech/koin/global/host/ServerURL.java create mode 100644 src/main/java/in/koreatech/koin/global/host/ServerURLArgumentResolver.java create mode 100644 src/main/java/in/koreatech/koin/global/host/ServerURLContext.java create mode 100644 src/main/java/in/koreatech/koin/global/host/ServerURLInterceptor.java diff --git a/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java b/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java index 3130176d4..4081ce874 100644 --- a/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java +++ b/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java @@ -1,11 +1,10 @@ package in.koreatech.koin.domain.user.controller; -import static in.koreatech.koin.domain.user.model.UserType.COOP; -import static in.koreatech.koin.domain.user.model.UserType.OWNER; -import static in.koreatech.koin.domain.user.model.UserType.STUDENT; +import static in.koreatech.koin.domain.user.model.UserType.*; import java.net.URI; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -16,6 +15,7 @@ import org.springframework.web.bind.annotation.RestController; import in.koreatech.koin.domain.user.dto.EmailCheckExistsRequest; +import in.koreatech.koin.domain.user.dto.FindPasswordRequest; import in.koreatech.koin.domain.user.dto.NicknameCheckExistsRequest; import in.koreatech.koin.domain.user.dto.StudentResponse; import in.koreatech.koin.domain.user.dto.StudentUpdateRequest; @@ -27,6 +27,7 @@ import in.koreatech.koin.domain.user.service.StudentService; import in.koreatech.koin.domain.user.service.UserService; import in.koreatech.koin.global.auth.Auth; +import in.koreatech.koin.global.host.ServerURL; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -104,4 +105,14 @@ public ResponseEntity checkDuplicationOfNickname( userService.checkUserNickname(request); return ResponseEntity.ok().build(); } + + @PostMapping("/user/find/password") + public ResponseEntity findePassword( + @ModelAttribute("email") + @Valid FindPasswordRequest request, + @ServerURL String serverURL + ) { + studentService.findPassword(request, serverURL); + return new ResponseEntity<>(HttpStatusCode.valueOf(201)); + } } diff --git a/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java b/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java index adf80fd52..22cc3f543 100644 --- a/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java +++ b/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java @@ -60,7 +60,7 @@ public void checkDepartmentValid(String department) { } } - public void sendFindPasswordMail(FindPasswordRequest request, String serverURL) { + public void findPassword(FindPasswordRequest request, String serverURL) { User user = userRepository.getByEmail(request.email()); if (user == null) { throw UserNotFoundException.withDetail("존재하지 않는 이메일입니다." + request.email()); diff --git a/src/main/java/in/koreatech/koin/domain/user/service/UserService.java b/src/main/java/in/koreatech/koin/domain/user/service/UserService.java index 3a7dc9676..69872a2ad 100644 --- a/src/main/java/in/koreatech/koin/domain/user/service/UserService.java +++ b/src/main/java/in/koreatech/koin/domain/user/service/UserService.java @@ -92,8 +92,4 @@ public void checkUserNickname(NicknameCheckExistsRequest request) { throw DuplicationEmailException.withDetail("nickname: " + request.nickname()); }); } - - public void sendResetTokenByEmailForFindPassword(String resetToken, String contextPath, String email) { - - } } diff --git a/src/main/java/in/koreatech/koin/global/host/ServerURL.java b/src/main/java/in/koreatech/koin/global/host/ServerURL.java new file mode 100644 index 000000000..0449c4e7a --- /dev/null +++ b/src/main/java/in/koreatech/koin/global/host/ServerURL.java @@ -0,0 +1,16 @@ +package in.koreatech.koin.global.host; + +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import io.swagger.v3.oas.annotations.Hidden; + +@Hidden // Swagger 문서에 표시하지 않음 +@Target(PARAMETER) +@Retention(RUNTIME) +public @interface ServerURL { + +} diff --git a/src/main/java/in/koreatech/koin/global/host/ServerURLArgumentResolver.java b/src/main/java/in/koreatech/koin/global/host/ServerURLArgumentResolver.java new file mode 100644 index 000000000..815159613 --- /dev/null +++ b/src/main/java/in/koreatech/koin/global/host/ServerURLArgumentResolver.java @@ -0,0 +1,29 @@ +package in.koreatech.koin.global.host; + +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class ServerURLArgumentResolver implements HandlerMethodArgumentResolver { + + private final ServerURLContext serverURLContext; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(ServerURL.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + + return serverURLContext.getServerURL(); + } +} diff --git a/src/main/java/in/koreatech/koin/global/host/ServerURLContext.java b/src/main/java/in/koreatech/koin/global/host/ServerURLContext.java new file mode 100644 index 000000000..b1297051b --- /dev/null +++ b/src/main/java/in/koreatech/koin/global/host/ServerURLContext.java @@ -0,0 +1,19 @@ +package in.koreatech.koin.global.host; + +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.RequestScope; + +@Component +@RequestScope +public class ServerURLContext { + + private String serverURL; + + public String getServerURL() { + return serverURL; + } + + public void setServerURL(String host) { + this.serverURL = host; + } +} diff --git a/src/main/java/in/koreatech/koin/global/host/ServerURLInterceptor.java b/src/main/java/in/koreatech/koin/global/host/ServerURLInterceptor.java new file mode 100644 index 000000000..04e5d546a --- /dev/null +++ b/src/main/java/in/koreatech/koin/global/host/ServerURLInterceptor.java @@ -0,0 +1,32 @@ +package in.koreatech.koin.global.host; + +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class ServerURLInterceptor implements HandlerInterceptor { + + private final ServerURLContext serverURLContext; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String serverURL = getServerURL(request); + serverURLContext.setServerURL(serverURL); + return true; + } + + public String getServerURL(HttpServletRequest request) { + String scheme = request.getScheme(); + String serverName = request.getServerName(); + int serverPort = request.getServerPort(); + + return (serverPort != 80 && serverPort != 443) ? + String.format("%s://%s:%d", scheme, serverName, serverPort) : + String.format("%s://%s", scheme, serverName); + } +} From 96e19b36ad242fdb0fe016e7d63ce49b66ad6bcc Mon Sep 17 00:00:00 2001 From: daheeParkk Date: Fri, 5 Apr 2024 02:44:29 +0900 Subject: [PATCH 04/12] =?UTF-8?q?feat:=20webConfig=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/user/controller/UserController.java | 2 +- .../java/in/koreatech/koin/global/config/WebConfig.java | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java b/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java index 4081ce874..57175f5b1 100644 --- a/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java +++ b/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java @@ -107,7 +107,7 @@ public ResponseEntity checkDuplicationOfNickname( } @PostMapping("/user/find/password") - public ResponseEntity findePassword( + public ResponseEntity findPassword( @ModelAttribute("email") @Valid FindPasswordRequest request, @ServerURL String serverURL diff --git a/src/main/java/in/koreatech/koin/global/config/WebConfig.java b/src/main/java/in/koreatech/koin/global/config/WebConfig.java index 0d9f78fc1..da78a1b55 100644 --- a/src/main/java/in/koreatech/koin/global/config/WebConfig.java +++ b/src/main/java/in/koreatech/koin/global/config/WebConfig.java @@ -1,6 +1,5 @@ package in.koreatech.koin.global.config; - import java.util.List; import org.springframework.context.annotation.Configuration; @@ -13,6 +12,8 @@ import in.koreatech.koin.global.auth.ExtractAuthenticationInterceptor; import in.koreatech.koin.global.auth.UserIdArgumentResolver; import in.koreatech.koin.global.domain.upload.controller.ImageUploadDomainEnumConverter; +import in.koreatech.koin.global.host.ServerURLArgumentResolver; +import in.koreatech.koin.global.host.ServerURLInterceptor; import in.koreatech.koin.global.ipaddress.IpAddressArgumentResolver; import in.koreatech.koin.global.ipaddress.IpAddressInterceptor; import lombok.RequiredArgsConstructor; @@ -26,6 +27,8 @@ public class WebConfig implements WebMvcConfigurer { private final UserIdArgumentResolver userIdArgumentResolver; private final AuthArgumentResolver authArgumentResolver; private final IpAddressInterceptor ipAddressInterceptor; + private final ServerURLArgumentResolver serverURLArgumentResolver; + private final ServerURLInterceptor serverURLInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { @@ -35,6 +38,9 @@ public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(ipAddressInterceptor) .addPathPatterns("/**") .order(1); + registry.addInterceptor(serverURLInterceptor) + .addPathPatterns("/**") + .order(2); } @Override @@ -42,6 +48,7 @@ public void addArgumentResolvers(List resolvers) resolvers.add(authArgumentResolver); resolvers.add(ipAddressArgumentResolver); resolvers.add(userIdArgumentResolver); + resolvers.add(serverURLArgumentResolver); } @Override From 75df5d1d2db8003ec2e2e8b482d42336146a9125 Mon Sep 17 00:00:00 2001 From: daheeParkk Date: Fri, 5 Apr 2024 05:16:57 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat:=20resetToken=EC=A0=84=EB=8B=AC?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 3 +-- .../koin/domain/user/model/User.java | 13 ++++++--- .../domain/user/service/StudentService.java | 1 + .../LocalDateTimeAttributeConverter.java | 27 +++++++++++++++++++ .../koin/global/domain/util/Sha256.java | 24 +++++++++++++++++ ...nt_change_password_certificate_button.html | 3 ++- 6 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 src/main/java/in/koreatech/koin/global/config/LocalDateTimeAttributeConverter.java create mode 100644 src/main/java/in/koreatech/koin/global/domain/util/Sha256.java diff --git a/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java b/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java index 57175f5b1..98fcd03d0 100644 --- a/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java +++ b/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java @@ -108,8 +108,7 @@ public ResponseEntity checkDuplicationOfNickname( @PostMapping("/user/find/password") public ResponseEntity findPassword( - @ModelAttribute("email") - @Valid FindPasswordRequest request, + @RequestBody @Valid FindPasswordRequest request, @ServerURL String serverURL ) { studentService.findPassword(request, serverURL); diff --git a/src/main/java/in/koreatech/koin/domain/user/model/User.java b/src/main/java/in/koreatech/koin/domain/user/model/User.java index 758b7bf36..9ca9ee941 100644 --- a/src/main/java/in/koreatech/koin/domain/user/model/User.java +++ b/src/main/java/in/koreatech/koin/domain/user/model/User.java @@ -9,6 +9,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import in.koreatech.koin.global.domain.BaseEntity; +import in.koreatech.koin.global.domain.util.Sha256; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -94,7 +95,6 @@ public class User extends BaseEntity { @Column(name = "reset_token") private String resetToken; - @Size(max = 255) @Column(name = "reset_expired_at") private String resetExpiredAt; @@ -103,9 +103,9 @@ public class User extends BaseEntity { @Builder private User(String password, String nickname, String name, String phoneNumber, UserType userType, - String email, UserGender gender, Boolean isAuthed, LocalDateTime lastLoggedAt, String profileImageUrl, - Boolean isDeleted, String authToken, String authExpiredAt, String resetToken, String resetExpiredAt, - String deviceToken) { + String email, UserGender gender, Boolean isAuthed, LocalDateTime lastLoggedAt, String profileImageUrl, + Boolean isDeleted, String authToken, String authExpiredAt, String resetToken, String resetExpiredAt, + String deviceToken) { this.password = password; this.nickname = nickname; this.name = name; @@ -144,6 +144,11 @@ public void updatePassword(PasswordEncoder passwordEncoder, String password) { this.password = passwordEncoder.encode(password); } + public void generateResetTokenForFindPassword() { + this.resetExpiredAt = LocalDateTime.now().plusHours(1).toString(); + this.resetToken = Sha256.encode(this.email + this.resetExpiredAt); + } + public void update(String nickname, String name, String phoneNumber, UserGender gender) { this.nickname = nickname; this.name = name; diff --git a/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java b/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java index 22cc3f543..b8de5b267 100644 --- a/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java +++ b/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java @@ -65,6 +65,7 @@ public void findPassword(FindPasswordRequest request, String serverURL) { if (user == null) { throw UserNotFoundException.withDetail("존재하지 않는 이메일입니다." + request.email()); } + user.generateResetTokenForFindPassword(); mailService.sendMail(request.email(), new StudentPasswordChangeData(serverURL, user.getResetToken())); } } diff --git a/src/main/java/in/koreatech/koin/global/config/LocalDateTimeAttributeConverter.java b/src/main/java/in/koreatech/koin/global/config/LocalDateTimeAttributeConverter.java new file mode 100644 index 000000000..0e837b0e6 --- /dev/null +++ b/src/main/java/in/koreatech/koin/global/config/LocalDateTimeAttributeConverter.java @@ -0,0 +1,27 @@ +package in.koreatech.koin.global.config; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter(autoApply = true) +public class LocalDateTimeAttributeConverter implements AttributeConverter { + + private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + @Override + public String convertToDatabaseColumn(LocalDateTime localDateTime) { + if (localDateTime == null) + return null; + return localDateTime.format(formatter); + } + + @Override + public LocalDateTime convertToEntityAttribute(String dbData) { + if (dbData == null) + return null; + return LocalDateTime.parse(dbData, formatter); + } +} diff --git a/src/main/java/in/koreatech/koin/global/domain/util/Sha256.java b/src/main/java/in/koreatech/koin/global/domain/util/Sha256.java new file mode 100644 index 000000000..3fd9a015d --- /dev/null +++ b/src/main/java/in/koreatech/koin/global/domain/util/Sha256.java @@ -0,0 +1,24 @@ +package in.koreatech.koin.global.domain.util; + +import java.security.MessageDigest; + +public class Sha256 { + public static String encode(String password) { + String SHA = ""; + try { + MessageDigest sh = MessageDigest.getInstance("SHA-256"); + sh.update(password.getBytes()); + byte byteData[] = sh.digest(); + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < byteData.length; i++) { + sb.append(Integer.toString((byteData[i] & 0xff) + 0x100, 16).substring(1)); + } + SHA = sb.toString(); + + } catch (Exception e) { + e.printStackTrace(); + SHA = null; + } + return SHA; + } +} diff --git a/src/main/resources/mail/student_change_password_certificate_button.html b/src/main/resources/mail/student_change_password_certificate_button.html index 942b054e9..9617aba32 100644 --- a/src/main/resources/mail/student_change_password_certificate_button.html +++ b/src/main/resources/mail/student_change_password_certificate_button.html @@ -39,7 +39,8 @@ text-decoration: none; display: inline-block; font-size: 16px;" class="btn" - href="${contextPath}/user/change/password/config?reset_token=${resetToken}">비밀번호 변경하기 + th:href="@{|${contextPath}/user/change/password/config?reset_token=${resetToken}|}">비밀번호 + 변경하기 From 2658e0a4ddab29bf4c21b9b1081963e6b4e02297 Mon Sep 17 00:00:00 2001 From: daheeParkk Date: Mon, 8 Apr 2024 19:22:26 +0900 Subject: [PATCH 06/12] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 8 +++ .../koin/domain/user/model/User.java | 3 +- .../user/repository/UserRepository.java | 7 +++ .../domain/user/service/StudentService.java | 12 ++-- .../host/ServerURLArgumentResolver.java | 1 - .../mail/change_password_config.html | 62 +++++++++++++++++++ 6 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 src/main/resources/mail/change_password_config.html diff --git a/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java b/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java index 98fcd03d0..131b1d806 100644 --- a/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java +++ b/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java @@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import in.koreatech.koin.domain.user.dto.EmailCheckExistsRequest; @@ -114,4 +115,11 @@ public ResponseEntity findPassword( studentService.findPassword(request, serverURL); return new ResponseEntity<>(HttpStatusCode.valueOf(201)); } + + @GetMapping("/user/change/password/config") + public String checkResetToken( + @RequestParam("reset_token") String resetToken + ) { + return studentService.checkResetToken(resetToken); + } } diff --git a/src/main/java/in/koreatech/koin/domain/user/model/User.java b/src/main/java/in/koreatech/koin/domain/user/model/User.java index 9ca9ee941..2e7b24dbe 100644 --- a/src/main/java/in/koreatech/koin/domain/user/model/User.java +++ b/src/main/java/in/koreatech/koin/domain/user/model/User.java @@ -9,7 +9,6 @@ import org.springframework.security.crypto.password.PasswordEncoder; import in.koreatech.koin.global.domain.BaseEntity; -import in.koreatech.koin.global.domain.util.Sha256; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -146,7 +145,7 @@ public void updatePassword(PasswordEncoder passwordEncoder, String password) { public void generateResetTokenForFindPassword() { this.resetExpiredAt = LocalDateTime.now().plusHours(1).toString(); - this.resetToken = Sha256.encode(this.email + this.resetExpiredAt); + this.resetToken = this.email + this.resetExpiredAt; } public void update(String nickname, String name, String phoneNumber, UserGender gender) { diff --git a/src/main/java/in/koreatech/koin/domain/user/repository/UserRepository.java b/src/main/java/in/koreatech/koin/domain/user/repository/UserRepository.java index b17508f4f..14c7e15d8 100644 --- a/src/main/java/in/koreatech/koin/domain/user/repository/UserRepository.java +++ b/src/main/java/in/koreatech/koin/domain/user/repository/UserRepository.java @@ -17,6 +17,8 @@ public interface UserRepository extends Repository { Optional findByNickname(String nickname); + Optional findAllByResetToken(String resetToken); + default User getByEmail(String email) { return findByEmail(email) .orElseThrow(() -> UserNotFoundException.withDetail("email: " + email)); @@ -32,6 +34,11 @@ default User getByNickname(String nickname) { .orElseThrow(() -> UserNotFoundException.withDetail("nickname: " + nickname)); } + default User getByResetToken(String resetToken) { + return findAllByResetToken(resetToken) + .orElseThrow(() -> UserNotFoundException.withDetail("resetToken: " + resetToken)); + } + boolean existsByNickname(String nickname); void delete(User user); diff --git a/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java b/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java index b8de5b267..fbebe787b 100644 --- a/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java +++ b/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java @@ -9,7 +9,6 @@ import in.koreatech.koin.domain.user.dto.StudentUpdateResponse; import in.koreatech.koin.domain.user.exception.DuplicationNicknameException; import in.koreatech.koin.domain.user.exception.StudentDepartmentNotValidException; -import in.koreatech.koin.domain.user.exception.UserNotFoundException; import in.koreatech.koin.domain.user.model.Student; import in.koreatech.koin.domain.user.model.StudentDepartment; import in.koreatech.koin.domain.user.model.User; @@ -62,10 +61,13 @@ public void checkDepartmentValid(String department) { public void findPassword(FindPasswordRequest request, String serverURL) { User user = userRepository.getByEmail(request.email()); - if (user == null) { - throw UserNotFoundException.withDetail("존재하지 않는 이메일입니다." + request.email()); - } user.generateResetTokenForFindPassword(); - mailService.sendMail(request.email(), new StudentPasswordChangeData(serverURL, user.getResetToken())); + User authedUser = userRepository.save(user); + mailService.sendMail(request.email(), new StudentPasswordChangeData(serverURL, authedUser.getResetToken())); + } + + public String checkResetToken(String resetToken) { + User user = userRepository.getByResetToken(resetToken); + return "change_password_config"; } } diff --git a/src/main/java/in/koreatech/koin/global/host/ServerURLArgumentResolver.java b/src/main/java/in/koreatech/koin/global/host/ServerURLArgumentResolver.java index 815159613..7ce6fd13d 100644 --- a/src/main/java/in/koreatech/koin/global/host/ServerURLArgumentResolver.java +++ b/src/main/java/in/koreatech/koin/global/host/ServerURLArgumentResolver.java @@ -23,7 +23,6 @@ public boolean supportsParameter(MethodParameter parameter) { @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { - return serverURLContext.getServerURL(); } } diff --git a/src/main/resources/mail/change_password_config.html b/src/main/resources/mail/change_password_config.html new file mode 100644 index 000000000..4916473b3 --- /dev/null +++ b/src/main/resources/mail/change_password_config.html @@ -0,0 +1,62 @@ + + + + + + 코인 이메일 인증 폼 + + + + + + + + + + + + + + +
+ Creating Email Magic +
+ + + + + + + + +
+ 비밀번호 변경 안내 +
+ 안녕하세요.

+ 한국기술교육대학교 커뮤니티 코인 운영팀입니다.
+
+ 패스워드를 입력해주세요.(6자이상 18자 이하의 특수문자를 포함)

+ +
+ + 비밀번호 입력 :

+ 비밀번호 확인 :

+

+
+
+
+ + + + +
+ Copyright BCSD Lab, TEAM_KAP All rights reserved. +
+
+ + + + + From b9dc0cb8b562afdd2618cf794663ade2d35c1567 Mon Sep 17 00:00:00 2001 From: daheeParkk Date: Mon, 8 Apr 2024 21:36:33 +0900 Subject: [PATCH 07/12] =?UTF-8?q?feat:=20=EB=B2=84=ED=8A=BC=20=EB=88=84?= =?UTF-8?q?=EB=A5=BC=20=EB=95=8C=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 19 +++++++- .../domain/user/service/StudentService.java | 13 +++++- .../mail/change_password_config.html | 4 +- .../resources/static/js/password.check.js | 45 +++++++++++++++++++ src/main/resources/static/js/sha256.min.js | 1 + 5 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 src/main/resources/static/js/password.check.js create mode 100644 src/main/resources/static/js/sha256.min.js diff --git a/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java b/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java index 131b1d806..1ff31bc72 100644 --- a/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java +++ b/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java @@ -3,9 +3,12 @@ import static in.koreatech.koin.domain.user.model.UserType.*; import java.net.URI; +import java.util.HashMap; +import java.util.Map; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; @@ -13,7 +16,6 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; import in.koreatech.koin.domain.user.dto.EmailCheckExistsRequest; import in.koreatech.koin.domain.user.dto.FindPasswordRequest; @@ -32,7 +34,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -@RestController +@Controller @RequiredArgsConstructor public class UserController implements UserApi { @@ -122,4 +124,17 @@ public String checkResetToken( ) { return studentService.checkResetToken(resetToken); } + + @PostMapping("/user/change/password/submit") + public Map changePassword( + @RequestBody Map params, + @RequestParam("reset_token") String resetToken + ) { + String password = params.get("password").toString(); + boolean success = studentService.changePassword(password, resetToken); + + return new HashMap() {{ + put("success", success); + }}; + } } diff --git a/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java b/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java index fbebe787b..cedcf0f6f 100644 --- a/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java +++ b/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java @@ -1,5 +1,6 @@ package in.koreatech.koin.domain.user.service; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -24,6 +25,7 @@ @Transactional(readOnly = true) public class StudentService { + private final PasswordEncoder passwordEncoder; private final StudentRepository studentRepository; private final UserRepository userRepository; private final MailService mailService; @@ -59,6 +61,7 @@ public void checkDepartmentValid(String department) { } } + @Transactional public void findPassword(FindPasswordRequest request, String serverURL) { User user = userRepository.getByEmail(request.email()); user.generateResetTokenForFindPassword(); @@ -68,6 +71,14 @@ public void findPassword(FindPasswordRequest request, String serverURL) { public String checkResetToken(String resetToken) { User user = userRepository.getByResetToken(resetToken); - return "change_password_config"; + return "change_password_config.html"; + } + + @Transactional + public boolean changePassword(String password, String resetToken) { + User authedUser = userRepository.getByResetToken(resetToken); + authedUser.updatePassword(passwordEncoder, password); + userRepository.save(authedUser); + return true; } } diff --git a/src/main/resources/mail/change_password_config.html b/src/main/resources/mail/change_password_config.html index 4916473b3..d66cc9df7 100644 --- a/src/main/resources/mail/change_password_config.html +++ b/src/main/resources/mail/change_password_config.html @@ -56,7 +56,7 @@ - - + + diff --git a/src/main/resources/static/js/password.check.js b/src/main/resources/static/js/password.check.js new file mode 100644 index 000000000..415bc8a91 --- /dev/null +++ b/src/main/resources/static/js/password.check.js @@ -0,0 +1,45 @@ +function validatePasswordFormat(password) { + var regExp = new RegExp('^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{6,18}$', 'g'); + if (!regExp.exec(password)) return false; + return true; +} + +var $userPw = $('#password'); +var $userPwConfirm = $('#password_confirm'); +var $requestUrl = $('#requestUrl'); + +$("#submitButton").click(function () { + var userPw = $.trim($userPw.val()); + var userPwConfirm = $.trim($userPwConfirm.val()); + + if (userPw !== userPwConfirm) { + alert('패스워드가 일치하지 않습니다. 다시 입력해주세요.'); + return false; + } + + if (validatePasswordFormat(userPw) === false) { + alert('특수문자를 포함한 영어와 숫자 6~18 자리를 입력하세요'); + return false; + } + + $.ajax({ + url: $requestUrl.val(), + type: 'post', + contentType:'application/json; charset=utf-8', + data: JSON.stringify({password: sha256(userPw)}), + success: function (response) { + if (response.success === true) { + alert('비밀번호 변경 성공!\n변경된 비밀번호로 로그인해주세요.'); + } + else { + alert('유효시간이 만료되었습니다.\n메일을 재전송하여 진행해주세요.'); + } + location.href = '//koreatech.in'; + }, + error: function (a, b, c) { + alert('서버와의 통신 중 오류가 발생했습니다.'); + } + }); + + return false; +}); diff --git a/src/main/resources/static/js/sha256.min.js b/src/main/resources/static/js/sha256.min.js new file mode 100644 index 000000000..becbd72b0 --- /dev/null +++ b/src/main/resources/static/js/sha256.min.js @@ -0,0 +1 @@ +var sha256=function a(b){function c(a,b){return a>>>b|a<<32-b}for(var d,e,f=Math.pow,g=f(2,32),h="length",i="",j=[],k=8*b[h],l=a.h=a.h||[],m=a.k=a.k||[],n=m[h],o={},p=2;64>n;p++)if(!o[p]){for(d=0;313>d;d+=p)o[d]=p;l[n]=f(p,.5)*g|0,m[n++]=f(p,1/3)*g|0}for(b+="\x80";b[h]%64-56;)b+="\x00";for(d=0;d>8)return;j[d>>2]|=e<<(3-d)%4*8}for(j[j[h]]=k/g|0,j[j[h]]=k,e=0;ed;d++){var s=q[d-15],t=q[d-2],u=l[0],v=l[4],w=l[7]+(c(v,6)^c(v,11)^c(v,25))+(v&l[5]^~v&l[6])+m[d]+(q[d]=16>d?q[d]:q[d-16]+(c(s,7)^c(s,18)^s>>>3)+q[d-7]+(c(t,17)^c(t,19)^t>>>10)|0),x=(c(u,2)^c(u,13)^c(u,22))+(u&l[1]^u&l[2]^l[1]&l[2]);l=[w+x|0].concat(l),l[4]=l[4]+w|0}for(d=0;8>d;d++)l[d]=l[d]+r[d]|0}for(d=0;8>d;d++)for(e=3;e+1;e--){var y=l[d]>>8*e&255;i+=(16>y?0:"")+y.toString(16)}return i}; \ No newline at end of file From 6902e3374fcc8eb7ad3077e42942d5b21e4abbe5 Mon Sep 17 00:00:00 2001 From: duehee <149302959+duehee@users.noreply.github.com> Date: Mon, 8 Apr 2024 22:16:23 +0900 Subject: [PATCH 08/12] =?UTF-8?q?chore=20:=20=EA=B6=8C=ED=95=9C=20static?= =?UTF-8?q?=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/koreatech/koin/domain/user/controller/UserApi.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/user/controller/UserApi.java b/src/main/java/in/koreatech/koin/domain/user/controller/UserApi.java index 068aebdaf..6e7c1671d 100644 --- a/src/main/java/in/koreatech/koin/domain/user/controller/UserApi.java +++ b/src/main/java/in/koreatech/koin/domain/user/controller/UserApi.java @@ -1,8 +1,6 @@ package in.koreatech.koin.domain.user.controller; -import static in.koreatech.koin.domain.user.model.UserType.COOP; -import static in.koreatech.koin.domain.user.model.UserType.OWNER; -import static in.koreatech.koin.domain.user.model.UserType.STUDENT; +import static in.koreatech.koin.domain.user.model.UserType.*; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -187,4 +185,5 @@ ResponseEntity checkDuplicationOfNickname( ResponseEntity getAuth( @Auth(permit = {STUDENT, OWNER, COOP}) Long userId ); + } From 9c95dcd8dbd95e2048f41cbce10a41a98961fb3d Mon Sep 17 00:00:00 2001 From: duehee <149302959+duehee@users.noreply.github.com> Date: Mon, 8 Apr 2024 22:25:20 +0900 Subject: [PATCH 09/12] =?UTF-8?q?chore=20:=20LocalDateTime=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20?= =?UTF-8?q?=EB=AF=B8=EC=82=AC=EC=9A=A9=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/user/controller/UserApi.java | 17 +++++++++++++ .../koin/domain/user/model/User.java | 13 +++++----- .../koin/global/domain/util/Sha256.java | 24 ------------------- 3 files changed, 24 insertions(+), 30 deletions(-) delete mode 100644 src/main/java/in/koreatech/koin/global/domain/util/Sha256.java diff --git a/src/main/java/in/koreatech/koin/domain/user/controller/UserApi.java b/src/main/java/in/koreatech/koin/domain/user/controller/UserApi.java index 6e7c1671d..ffd39f05f 100644 --- a/src/main/java/in/koreatech/koin/domain/user/controller/UserApi.java +++ b/src/main/java/in/koreatech/koin/domain/user/controller/UserApi.java @@ -12,6 +12,7 @@ import in.koreatech.koin.domain.user.dto.AuthResponse; import in.koreatech.koin.domain.user.dto.EmailCheckExistsRequest; +import in.koreatech.koin.domain.user.dto.FindPasswordRequest; import in.koreatech.koin.domain.user.dto.NicknameCheckExistsRequest; import in.koreatech.koin.domain.user.dto.StudentRegisterRequest; import in.koreatech.koin.domain.user.dto.StudentResponse; @@ -186,4 +187,20 @@ ResponseEntity getAuth( @Auth(permit = {STUDENT, OWNER, COOP}) Long userId ); + @ApiResponses( + value = { + @ApiResponse(responseCode = "201"), + @ApiResponse(responseCode = "400", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))) + } + ) + @Operation(summary = "비밀번호 초기(변경) 메일 발송") + @PostMapping("/user/find/password") + ResponseEntity findPassword( + @RequestBody @Valid FindPasswordRequest findPasswordRequest, + @ServerURL String serverURL + ); + } diff --git a/src/main/java/in/koreatech/koin/domain/user/model/User.java b/src/main/java/in/koreatech/koin/domain/user/model/User.java index b6a79f630..c1d650939 100644 --- a/src/main/java/in/koreatech/koin/domain/user/model/User.java +++ b/src/main/java/in/koreatech/koin/domain/user/model/User.java @@ -87,17 +87,17 @@ public class User extends BaseEntity { @Column(name = "auth_token") private String authToken; - @Size(max = 255) + @Convert(converter = LocalDateTimeAttributeConverter.class) @Column(name = "auth_expired_at") - private String authExpiredAt; + private LocalDateTime authExpiredAt; @Size(max = 255) @Column(name = "reset_token") private String resetToken; - @Size(max = 255) + @Convert(converter = LocalDateTimeAttributeConverter.class) @Column(name = "reset_expired_at") - private String resetExpiredAt; + private LocalDateTime resetExpiredAt; @Column(name = "device_token", nullable = true) private String deviceToken; @@ -105,7 +105,8 @@ public class User extends BaseEntity { @Builder private User(String password, String nickname, String name, String phoneNumber, UserType userType, String email, UserGender gender, Boolean isAuthed, LocalDateTime lastLoggedAt, String profileImageUrl, - Boolean isDeleted, String authToken, String authExpiredAt, String resetToken, String resetExpiredAt, + Boolean isDeleted, String authToken, LocalDateTime authExpiredAt, String resetToken, + LocalDateTime resetExpiredAt, String deviceToken) { this.password = password; this.nickname = nickname; @@ -146,7 +147,7 @@ public void updatePassword(PasswordEncoder passwordEncoder, String password) { } public void generateResetTokenForFindPassword() { - this.resetExpiredAt = LocalDateTime.now().plusHours(1).toString(); + this.resetExpiredAt = LocalDateTime.now().plusHours(1); this.resetToken = this.email + this.resetExpiredAt; } diff --git a/src/main/java/in/koreatech/koin/global/domain/util/Sha256.java b/src/main/java/in/koreatech/koin/global/domain/util/Sha256.java deleted file mode 100644 index 3fd9a015d..000000000 --- a/src/main/java/in/koreatech/koin/global/domain/util/Sha256.java +++ /dev/null @@ -1,24 +0,0 @@ -package in.koreatech.koin.global.domain.util; - -import java.security.MessageDigest; - -public class Sha256 { - public static String encode(String password) { - String SHA = ""; - try { - MessageDigest sh = MessageDigest.getInstance("SHA-256"); - sh.update(password.getBytes()); - byte byteData[] = sh.digest(); - StringBuffer sb = new StringBuffer(); - for (int i = 0; i < byteData.length; i++) { - sb.append(Integer.toString((byteData[i] & 0xff) + 0x100, 16).substring(1)); - } - SHA = sb.toString(); - - } catch (Exception e) { - e.printStackTrace(); - SHA = null; - } - return SHA; - } -} From 2dd5707c9bac75cad2a0db1d510d2fd434e13299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=AE=E1=86=AB=E1=84=92?= =?UTF-8?q?=E1=85=A9?= Date: Mon, 8 Apr 2024 23:28:34 +0900 Subject: [PATCH 10/12] =?UTF-8?q?feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/user/controller/UserApi.java | 5 +- .../user/controller/UserController.java | 27 +++-- .../user/dto/UserPasswordChangeRequest.java | 8 ++ .../UserResetTokenExpiredException.java | 17 +++ .../koin/domain/user/model/User.java | 20 +++- .../domain/user/service/StudentService.java | 23 ++-- .../email/form/StudentPasswordChangeData.java | 2 +- .../host/ServerURLArgumentResolver.java | 3 +- .../mail/change_password_config.html | 103 +++++++++--------- .../resources/static/js/password.check.js | 69 ++++++------ 10 files changed, 157 insertions(+), 120 deletions(-) create mode 100644 src/main/java/in/koreatech/koin/domain/user/dto/UserPasswordChangeRequest.java create mode 100644 src/main/java/in/koreatech/koin/domain/user/exception/UserResetTokenExpiredException.java diff --git a/src/main/java/in/koreatech/koin/domain/user/controller/UserApi.java b/src/main/java/in/koreatech/koin/domain/user/controller/UserApi.java index ffd39f05f..dd147bdb7 100644 --- a/src/main/java/in/koreatech/koin/domain/user/controller/UserApi.java +++ b/src/main/java/in/koreatech/koin/domain/user/controller/UserApi.java @@ -1,6 +1,8 @@ package in.koreatech.koin.domain.user.controller; -import static in.koreatech.koin.domain.user.model.UserType.*; +import static in.koreatech.koin.domain.user.model.UserType.COOP; +import static in.koreatech.koin.domain.user.model.UserType.OWNER; +import static in.koreatech.koin.domain.user.model.UserType.STUDENT; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -202,5 +204,4 @@ ResponseEntity findPassword( @RequestBody @Valid FindPasswordRequest findPasswordRequest, @ServerURL String serverURL ); - } diff --git a/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java b/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java index 44b5e5f1c..c7ec25f64 100644 --- a/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java +++ b/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java @@ -1,10 +1,10 @@ package in.koreatech.koin.domain.user.controller; -import static in.koreatech.koin.domain.user.model.UserType.*; +import static in.koreatech.koin.domain.user.model.UserType.COOP; +import static in.koreatech.koin.domain.user.model.UserType.OWNER; +import static in.koreatech.koin.domain.user.model.UserType.STUDENT; import java.net.URI; -import java.util.HashMap; -import java.util.Map; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; @@ -29,6 +29,7 @@ import in.koreatech.koin.domain.user.dto.StudentUpdateResponse; import in.koreatech.koin.domain.user.dto.UserLoginRequest; import in.koreatech.koin.domain.user.dto.UserLoginResponse; +import in.koreatech.koin.domain.user.dto.UserPasswordChangeRequest; import in.koreatech.koin.domain.user.dto.UserTokenRefreshRequest; import in.koreatech.koin.domain.user.dto.UserTokenRefreshResponse; import in.koreatech.koin.domain.user.service.StudentService; @@ -107,7 +108,8 @@ public ResponseEntity checkUserEmailExist( @PostMapping("/user/student/register") public ResponseEntity studentRegister( @Valid @RequestBody StudentRegisterRequest request, - @ServerURL String serverURL) { + @ServerURL String serverURL + ) { studentService.studentRegister(request, serverURL); return ResponseEntity.ok().build(); } @@ -147,22 +149,19 @@ public ResponseEntity findPassword( } @GetMapping("/user/change/password/config") - public String checkResetToken( + public ModelAndView checkResetToken( + @ServerURL String serverUrl, @RequestParam("reset_token") String resetToken ) { - return studentService.checkResetToken(resetToken); + return studentService.checkResetToken(resetToken, serverUrl); } @PostMapping("/user/change/password/submit") - public Map changePassword( - @RequestBody Map params, + public ResponseEntity changePassword( + @RequestBody UserPasswordChangeRequest request, @RequestParam("reset_token") String resetToken ) { - String password = params.get("password").toString(); - boolean success = studentService.changePassword(password, resetToken); - - return new HashMap<>() {{ - put("success", success); - }}; + studentService.changePassword(request, resetToken); + return ResponseEntity.ok().build(); } } diff --git a/src/main/java/in/koreatech/koin/domain/user/dto/UserPasswordChangeRequest.java b/src/main/java/in/koreatech/koin/domain/user/dto/UserPasswordChangeRequest.java new file mode 100644 index 000000000..4041f0ffa --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/user/dto/UserPasswordChangeRequest.java @@ -0,0 +1,8 @@ +package in.koreatech.koin.domain.user.dto; + +public record UserPasswordChangeRequest( + + String password +) { + +} diff --git a/src/main/java/in/koreatech/koin/domain/user/exception/UserResetTokenExpiredException.java b/src/main/java/in/koreatech/koin/domain/user/exception/UserResetTokenExpiredException.java new file mode 100644 index 000000000..7e8153927 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/user/exception/UserResetTokenExpiredException.java @@ -0,0 +1,17 @@ +package in.koreatech.koin.domain.user.exception; + +import in.koreatech.koin.global.auth.exception.AuthenticationException; + +public class UserResetTokenExpiredException extends AuthenticationException { + + private static final String DEFAULT_MESSAGE = "비밀번호 재설정 토큰이 만료되었습니다."; + + public UserResetTokenExpiredException(String message) { + super(message); + } + + public static UserResetTokenExpiredException withDetail(String detail) { + String message = String.format("%s %s", DEFAULT_MESSAGE, detail); + return new UserResetTokenExpiredException(message); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/user/model/User.java b/src/main/java/in/koreatech/koin/domain/user/model/User.java index 7bc6e5ec0..e01ad7460 100644 --- a/src/main/java/in/koreatech/koin/domain/user/model/User.java +++ b/src/main/java/in/koreatech/koin/domain/user/model/User.java @@ -2,12 +2,14 @@ import static lombok.AccessLevel.PROTECTED; +import java.time.Clock; import java.time.LocalDateTime; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; import org.springframework.security.crypto.password.PasswordEncoder; +import in.koreatech.koin.domain.user.exception.UserResetTokenExpiredException; import in.koreatech.koin.global.config.LocalDateTimeAttributeConverter; import in.koreatech.koin.global.domain.BaseEntity; import jakarta.persistence.Column; @@ -105,10 +107,10 @@ public class User extends BaseEntity { @Builder private User(String password, String nickname, String name, String phoneNumber, UserType userType, - String email, UserGender gender, boolean isAuthed, LocalDateTime lastLoggedAt, String profileImageUrl, - Boolean isDeleted, String authToken, LocalDateTime authExpiredAt, String resetToken, - LocalDateTime resetExpiredAt, - String deviceToken) { + String email, UserGender gender, boolean isAuthed, LocalDateTime lastLoggedAt, String profileImageUrl, + Boolean isDeleted, String authToken, LocalDateTime authExpiredAt, String resetToken, + LocalDateTime resetExpiredAt, + String deviceToken) { this.password = password; this.nickname = nickname; this.name = name; @@ -147,8 +149,8 @@ public void updatePassword(PasswordEncoder passwordEncoder, String password) { this.password = passwordEncoder.encode(password); } - public void generateResetTokenForFindPassword() { - this.resetExpiredAt = LocalDateTime.now().plusHours(1); + public void generateResetTokenForFindPassword(Clock clock) { + this.resetExpiredAt = LocalDateTime.now(clock).plusHours(1); this.resetToken = this.email + this.resetExpiredAt; } @@ -162,4 +164,10 @@ public void update(String nickname, String name, String phoneNumber, UserGender public void auth() { this.isAuthed = true; } + + public void validateResetToken() { + if (resetExpiredAt.isBefore(LocalDateTime.now())) { + throw UserResetTokenExpiredException.withDetail("resetToken: " + resetToken); + } + } } diff --git a/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java b/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java index 711be75ae..fee01fd92 100644 --- a/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java +++ b/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java @@ -6,17 +6,17 @@ import org.joda.time.LocalDateTime; import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.servlet.ModelAndView; import in.koreatech.koin.domain.user.dto.AuthTokenRequest; -import in.koreatech.koin.domain.user.dto.StudentRegisterRequest; import in.koreatech.koin.domain.user.dto.FindPasswordRequest; +import in.koreatech.koin.domain.user.dto.StudentRegisterRequest; import in.koreatech.koin.domain.user.dto.StudentResponse; import in.koreatech.koin.domain.user.dto.StudentUpdateRequest; import in.koreatech.koin.domain.user.dto.StudentUpdateResponse; +import in.koreatech.koin.domain.user.dto.UserPasswordChangeRequest; import in.koreatech.koin.domain.user.exception.DuplicationNicknameException; import in.koreatech.koin.domain.user.exception.StudentDepartmentNotValidException; import in.koreatech.koin.domain.user.exception.StudentNumberNotValidException; @@ -29,11 +29,10 @@ import in.koreatech.koin.domain.user.repository.StudentRepository; import in.koreatech.koin.domain.user.repository.UserRepository; import in.koreatech.koin.global.domain.email.exception.DuplicationEmailException; +import in.koreatech.koin.global.domain.email.form.StudentPasswordChangeData; import in.koreatech.koin.global.domain.email.form.StudentRegistrationData; import in.koreatech.koin.global.domain.email.model.EmailAddress; import in.koreatech.koin.global.domain.email.service.MailService; -import in.koreatech.koin.global.domain.email.form.StudentPasswordChangeData; -import in.koreatech.koin.global.domain.email.service.MailService; import lombok.RequiredArgsConstructor; @Service @@ -135,21 +134,23 @@ private void validateStudentNumber(String studentNumber) { @Transactional public void findPassword(FindPasswordRequest request, String serverURL) { User user = userRepository.getByEmail(request.email()); - user.generateResetTokenForFindPassword(); + user.generateResetTokenForFindPassword(clock); User authedUser = userRepository.save(user); mailService.sendMail(request.email(), new StudentPasswordChangeData(serverURL, authedUser.getResetToken())); } - public String checkResetToken(String resetToken) { - User user = userRepository.getByResetToken(resetToken); - return "change_password_config.html"; + public ModelAndView checkResetToken(String resetToken, String serverUrl) { + ModelAndView modelAndView = new ModelAndView("change_password_config"); + modelAndView.addObject("contextPath", serverUrl); + modelAndView.addObject("resetToken", resetToken); + return modelAndView; } @Transactional - public boolean changePassword(String password, String resetToken) { + public void changePassword(UserPasswordChangeRequest request, String resetToken) { User authedUser = userRepository.getByResetToken(resetToken); - authedUser.updatePassword(passwordEncoder, password); + authedUser.validateResetToken(); + authedUser.updatePassword(passwordEncoder, request.password()); userRepository.save(authedUser); - return true; } } diff --git a/src/main/java/in/koreatech/koin/global/domain/email/form/StudentPasswordChangeData.java b/src/main/java/in/koreatech/koin/global/domain/email/form/StudentPasswordChangeData.java index 3220ff211..3abb90d04 100644 --- a/src/main/java/in/koreatech/koin/global/domain/email/form/StudentPasswordChangeData.java +++ b/src/main/java/in/koreatech/koin/global/domain/email/form/StudentPasswordChangeData.java @@ -3,7 +3,7 @@ import java.util.Map; public class StudentPasswordChangeData implements MailFormData { - private static final String SUBJECT = "학생 비밀번호 변경"; + private static final String SUBJECT = "코인 패스워드 초기화 인증"; private static final String PATH = "student_change_password_certificate_button"; private final String contextPath; diff --git a/src/main/java/in/koreatech/koin/global/host/ServerURLArgumentResolver.java b/src/main/java/in/koreatech/koin/global/host/ServerURLArgumentResolver.java index 815159613..7f6dbe43e 100644 --- a/src/main/java/in/koreatech/koin/global/host/ServerURLArgumentResolver.java +++ b/src/main/java/in/koreatech/koin/global/host/ServerURLArgumentResolver.java @@ -22,8 +22,7 @@ public boolean supportsParameter(MethodParameter parameter) { @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { - + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { return serverURLContext.getServerURL(); } } diff --git a/src/main/resources/mail/change_password_config.html b/src/main/resources/mail/change_password_config.html index d8df53b0f..163452dcd 100644 --- a/src/main/resources/mail/change_password_config.html +++ b/src/main/resources/mail/change_password_config.html @@ -1,62 +1,63 @@ - - - 코인 이메일 인증 폼 - + + + 코인 이메일 인증 폼 + - - - - - -
- Creating Email Magic -
- - - - - - + + + +
- 비밀번호 변경 안내 -
- 안녕하세요.

- 한국기술교육대학교 커뮤니티 코인 운영팀입니다.
-
- 패스워드를 입력해주세요.(6자이상 18자 이하의 특수문자를 포함)

+ + + + + + - - - - +
+ Creating Email Magic +
+ + + + + + + -
- - 비밀번호 입력 :

- 비밀번호 확인 :

-

-
- - - -
+ 비밀번호 변경 안내 +
+ 안녕하세요.

+ 한국기술교육대학교 커뮤니티 코인 운영팀입니다.
+
+ 패스워드를 입력해주세요.(6자이상 18자 이하의 특수문자를 포함)

+
+ + 비밀번호 입력 :

+ 비밀번호 확인 :

+

+
+
-
- - - - -
- Copyright BCSD Lab, TEAM_KAP All rights reserved. -
-
+
+ + + + +
+ Copyright BCSD Lab, TEAM_KAP All rights reserved. +
+
- - + + diff --git a/src/main/resources/static/js/password.check.js b/src/main/resources/static/js/password.check.js index 415bc8a91..58eee4a12 100644 --- a/src/main/resources/static/js/password.check.js +++ b/src/main/resources/static/js/password.check.js @@ -1,7 +1,10 @@ function validatePasswordFormat(password) { - var regExp = new RegExp('^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{6,18}$', 'g'); - if (!regExp.exec(password)) return false; - return true; + var regExp = new RegExp( + '^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{6,18}$', 'g'); + if (!regExp.exec(password)) { + return false; + } + return true; } var $userPw = $('#password'); @@ -9,37 +12,37 @@ var $userPwConfirm = $('#password_confirm'); var $requestUrl = $('#requestUrl'); $("#submitButton").click(function () { - var userPw = $.trim($userPw.val()); - var userPwConfirm = $.trim($userPwConfirm.val()); - - if (userPw !== userPwConfirm) { - alert('패스워드가 일치하지 않습니다. 다시 입력해주세요.'); - return false; - } - - if (validatePasswordFormat(userPw) === false) { - alert('특수문자를 포함한 영어와 숫자 6~18 자리를 입력하세요'); - return false; - } + var userPw = $.trim($userPw.val()); + var userPwConfirm = $.trim($userPwConfirm.val()); - $.ajax({ - url: $requestUrl.val(), - type: 'post', - contentType:'application/json; charset=utf-8', - data: JSON.stringify({password: sha256(userPw)}), - success: function (response) { - if (response.success === true) { - alert('비밀번호 변경 성공!\n변경된 비밀번호로 로그인해주세요.'); - } - else { - alert('유효시간이 만료되었습니다.\n메일을 재전송하여 진행해주세요.'); - } - location.href = '//koreatech.in'; - }, - error: function (a, b, c) { - alert('서버와의 통신 중 오류가 발생했습니다.'); - } - }); + if (userPw !== userPwConfirm) { + alert('패스워드가 일치하지 않습니다. 다시 입력해주세요.'); + return false; + } + if (validatePasswordFormat(userPw) === false) { + alert('특수문자를 포함한 영어와 숫자 6~18 자리를 입력하세요'); return false; + } + + $.ajax({ + url: $requestUrl.val(), + type: 'post', + contentType: 'application/json; charset=utf-8', + data: JSON.stringify({password: sha256(userPw)}), + success: function (response) { + alert('비밀번호 변경 성공!\n변경된 비밀번호로 로그인해주세요.'); + location.href = '//koreatech.in'; + }, + error: function (jqXHR, textStatus, errorThrown) { + if (jqXHR.status === 401) { + // 401 에러에 대한 특정 로직 + alert('유효시간이 만료되었습니다.\n메일을 재전송하여 진행해주세요.'); + } else { + // 기타 에러 처리 + alert('서버와의 통신 중 오류가 발생했습니다.'); + } + } + }); + return false; }); From 5294f68a4b3f33a62d7ee237f5572a49b9dfbb36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=AE=E1=86=AB=E1=84=92?= =?UTF-8?q?=E1=85=A9?= Date: Mon, 8 Apr 2024 23:59:05 +0900 Subject: [PATCH 11/12] =?UTF-8?q?docs:=20hidden=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koreatech/koin/domain/user/controller/UserController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java b/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java index c7ec25f64..5876922aa 100644 --- a/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java +++ b/src/main/java/in/koreatech/koin/domain/user/controller/UserController.java @@ -36,6 +36,7 @@ import in.koreatech.koin.domain.user.service.UserService; import in.koreatech.koin.global.auth.Auth; import in.koreatech.koin.global.host.ServerURL; +import io.swagger.v3.oas.annotations.Hidden; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -156,6 +157,7 @@ public ModelAndView checkResetToken( return studentService.checkResetToken(resetToken, serverUrl); } + @Hidden @PostMapping("/user/change/password/submit") public ResponseEntity changePassword( @RequestBody UserPasswordChangeRequest request, From ca6e9279bedea46e281d5df0ca26e9dc082ed600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=AE=E1=86=AB=E1=84=92?= =?UTF-8?q?=E1=85=A9?= Date: Tue, 9 Apr 2024 00:09:38 +0900 Subject: [PATCH 12/12] =?UTF-8?q?style:=20=EA=B0=9C=ED=96=89=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/user/dto/UserPasswordChangeRequest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/in/koreatech/koin/domain/user/dto/UserPasswordChangeRequest.java b/src/main/java/in/koreatech/koin/domain/user/dto/UserPasswordChangeRequest.java index 4041f0ffa..e35a849a9 100644 --- a/src/main/java/in/koreatech/koin/domain/user/dto/UserPasswordChangeRequest.java +++ b/src/main/java/in/koreatech/koin/domain/user/dto/UserPasswordChangeRequest.java @@ -1,7 +1,10 @@ package in.koreatech.koin.domain.user.dto; -public record UserPasswordChangeRequest( +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +@JsonNaming(SnakeCaseStrategy.class) +public record UserPasswordChangeRequest( String password ) {