diff --git a/src/main/java/clap/server/adapter/inbound/security/WebSecurityUrl.java b/src/main/java/clap/server/adapter/inbound/security/WebSecurityUrl.java index d4cf2dd7..398db74d 100644 --- a/src/main/java/clap/server/adapter/inbound/security/WebSecurityUrl.java +++ b/src/main/java/clap/server/adapter/inbound/security/WebSecurityUrl.java @@ -13,6 +13,6 @@ private WebSecurityUrl() { "/swagger-ui/**", "/swagger" }; public static final String REISSUANCE_ENDPOINT = "/api/auths/reissuance"; - public static final String PASSWORD_EMAIL_ENDPOINT = "/api/verifications/**"; + public static final String PASSWORD_EMAIL_ENDPOINT = "/api/new-password"; public static final String TEMPORARY_TOKEN_ALLOWED_ENDPOINT = "/api/members/initial-password"; } diff --git a/src/main/java/clap/server/adapter/inbound/web/member/EmailVerificationController.java b/src/main/java/clap/server/adapter/inbound/web/auth/EmailVerificationController.java similarity index 84% rename from src/main/java/clap/server/adapter/inbound/web/member/EmailVerificationController.java rename to src/main/java/clap/server/adapter/inbound/web/auth/EmailVerificationController.java index 9d3a443a..9780e490 100644 --- a/src/main/java/clap/server/adapter/inbound/web/member/EmailVerificationController.java +++ b/src/main/java/clap/server/adapter/inbound/web/auth/EmailVerificationController.java @@ -1,7 +1,7 @@ -package clap.server.adapter.inbound.web.member; +package clap.server.adapter.inbound.web.auth; -import clap.server.adapter.inbound.web.dto.member.response.SendVerificationCodeRequest; -import clap.server.adapter.inbound.web.dto.member.response.VerifyCodeRequest; +import clap.server.adapter.inbound.web.dto.member.request.SendVerificationCodeRequest; +import clap.server.adapter.inbound.web.dto.member.request.VerifyCodeRequest; import clap.server.application.port.inbound.member.SendVerificationEmailUsecase; import clap.server.application.port.inbound.member.VerifyEmailCodeUsecase; import clap.server.common.annotation.architecture.WebAdapter; @@ -20,12 +20,14 @@ public class EmailVerificationController { private final SendVerificationEmailUsecase sendVerificationEmailUsecase; private final VerifyEmailCodeUsecase verifyEmailCodeUsecase; + @Deprecated @Operation(summary = "인증번호 전송 API") @PostMapping("/verifications/email") public void sendVerificationEmail(@RequestBody SendVerificationCodeRequest request) { sendVerificationEmailUsecase.sendVerificationCode(request); } + @Deprecated @Operation(summary = "인증번호 검증 API") @PostMapping("/verifications") public void sendVerificationEmail(@RequestBody VerifyCodeRequest request) { diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/member/request/SendInitialPasswordRequest.java b/src/main/java/clap/server/adapter/inbound/web/dto/member/request/SendInitialPasswordRequest.java new file mode 100644 index 00000000..6ffb8c89 --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/member/request/SendInitialPasswordRequest.java @@ -0,0 +1,11 @@ +package clap.server.adapter.inbound.web.dto.member.request; + +import jakarta.validation.constraints.NotBlank; + +public record SendInitialPasswordRequest( + @NotBlank + String name, + @NotBlank + String email +){ +} diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/member/response/SendVerificationCodeRequest.java b/src/main/java/clap/server/adapter/inbound/web/dto/member/request/SendVerificationCodeRequest.java similarity index 74% rename from src/main/java/clap/server/adapter/inbound/web/dto/member/response/SendVerificationCodeRequest.java rename to src/main/java/clap/server/adapter/inbound/web/dto/member/request/SendVerificationCodeRequest.java index c39abb58..c7c07367 100644 --- a/src/main/java/clap/server/adapter/inbound/web/dto/member/response/SendVerificationCodeRequest.java +++ b/src/main/java/clap/server/adapter/inbound/web/dto/member/request/SendVerificationCodeRequest.java @@ -1,4 +1,4 @@ -package clap.server.adapter.inbound.web.dto.member.response; +package clap.server.adapter.inbound.web.dto.member.request; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/member/response/VerifyCodeRequest.java b/src/main/java/clap/server/adapter/inbound/web/dto/member/request/VerifyCodeRequest.java similarity index 73% rename from src/main/java/clap/server/adapter/inbound/web/dto/member/response/VerifyCodeRequest.java rename to src/main/java/clap/server/adapter/inbound/web/dto/member/request/VerifyCodeRequest.java index 3234168e..f7df8e2f 100644 --- a/src/main/java/clap/server/adapter/inbound/web/dto/member/response/VerifyCodeRequest.java +++ b/src/main/java/clap/server/adapter/inbound/web/dto/member/request/VerifyCodeRequest.java @@ -1,5 +1,5 @@ -package clap.server.adapter.inbound.web.dto.member.response; +package clap.server.adapter.inbound.web.dto.member.request; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/clap/server/adapter/inbound/web/member/ResetPasswordController.java b/src/main/java/clap/server/adapter/inbound/web/member/ResetPasswordController.java index 9d6f986d..95b68994 100644 --- a/src/main/java/clap/server/adapter/inbound/web/member/ResetPasswordController.java +++ b/src/main/java/clap/server/adapter/inbound/web/member/ResetPasswordController.java @@ -1,11 +1,13 @@ package clap.server.adapter.inbound.web.member; import clap.server.adapter.inbound.security.service.SecurityUserDetails; +import clap.server.adapter.inbound.web.dto.member.request.SendInitialPasswordRequest; import clap.server.adapter.inbound.web.dto.member.request.UpdateInitialPasswordRequest; import clap.server.adapter.inbound.web.dto.member.request.UpdatePasswordRequest; import clap.server.adapter.inbound.web.dto.member.request.VerifyPasswordRequest; import clap.server.application.port.inbound.member.ResetInitialPasswordUsecase; import clap.server.application.port.inbound.member.ResetPasswordUsecase; +import clap.server.application.port.inbound.member.SendNewPasswordUsecase; import clap.server.application.port.inbound.member.VerifyPasswordUseCase; import clap.server.common.annotation.architecture.WebAdapter; import io.swagger.v3.oas.annotations.Operation; @@ -23,6 +25,7 @@ public class ResetPasswordController { private final ResetPasswordUsecase resetPasswordUsecase; private final ResetInitialPasswordUsecase resetInitialPasswordUsecase; private final VerifyPasswordUseCase verifyPasswordUseCase; + private final SendNewPasswordUsecase sendNewPasswordUsecase; @Operation(summary = "초기 로그인 후 비밀번호 재설정 API") @PatchMapping("/members/initial-password") @@ -45,4 +48,10 @@ public void verifyPassword(@AuthenticationPrincipal SecurityUserDetails userInfo @RequestBody @Valid VerifyPasswordRequest request) { verifyPasswordUseCase.verifyPassword(userInfo.getUserId(), request.password()); } + + @Operation(summary = "비밀번호 재설정 이메일 전송 API") + @PostMapping("/new-password") + public void sendNewPasswordEmail(@RequestBody @Valid SendInitialPasswordRequest request) { + sendNewPasswordUsecase.sendInitialPassword(request); + } } diff --git a/src/main/java/clap/server/adapter/outbound/api/EmailClient.java b/src/main/java/clap/server/adapter/outbound/api/EmailClient.java index bcb335e0..00ab7452 100644 --- a/src/main/java/clap/server/adapter/outbound/api/EmailClient.java +++ b/src/main/java/clap/server/adapter/outbound/api/EmailClient.java @@ -70,5 +70,22 @@ public void sendVerificationEmail(String memberEmail, String receiverName, Strin } } + @Override + public void sendNewPasswordEmail(String memberEmail, String receiverName, String newPassword) { + try { + MimeMessage mimeMessage = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8"); + + EmailTemplate template = emailTemplateBuilder.createNewPasswordTemplate(memberEmail, receiverName, newPassword); + helper.setTo(template.email()); + helper.setSubject(template.subject()); + helper.setText(template.body(), true); + + mailSender.send(mimeMessage); + } catch (Exception e) { + throw new AdapterException(NotificationErrorCode.EMAIL_SEND_FAILED); + } + } + } \ No newline at end of file diff --git a/src/main/java/clap/server/adapter/outbound/api/EmailTemplateBuilder.java b/src/main/java/clap/server/adapter/outbound/api/EmailTemplateBuilder.java index f235f3af..4411e7de 100644 --- a/src/main/java/clap/server/adapter/outbound/api/EmailTemplateBuilder.java +++ b/src/main/java/clap/server/adapter/outbound/api/EmailTemplateBuilder.java @@ -82,4 +82,15 @@ public EmailTemplate createVerificationCodeTemplate(String receiver, String rece String body = templateEngine.process(templateName, context); return new EmailTemplate(receiver, subject, body); } + + public EmailTemplate createNewPasswordTemplate(String receiver, String receiverName, String newPassword) { + Context context = new Context(); + String templateName = "new-password"; + String subject = "[TaskFlow] 비밀번호 재설정"; + context.setVariable("loginLink", "http://localhost:5173/login"); + context.setVariable("newPassword", newPassword); + context.setVariable("receiverName", receiverName); + String body = templateEngine.process(templateName, context); + return new EmailTemplate(receiver, subject, body); + } } \ No newline at end of file diff --git a/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java b/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java index 26e19c8f..e6fbafbe 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java @@ -86,12 +86,6 @@ public List findActiveManagers() { .collect(Collectors.toList()); } - @Override - public int getRemainingTasks(Long memberId) { - List targetStatuses = List.of(TaskStatus.IN_PROGRESS, TaskStatus.IN_REVIEWING); - return findTasksByMemberIdAndStatus(memberId, targetStatuses).size(); - } - @Override public List findTasksByMemberIdAndStatus(Long memberId, List taskStatuses) { List taskEntities = taskRepository.findByProcessor_MemberIdAndTaskStatusIn(memberId, taskStatuses); @@ -111,8 +105,13 @@ public Page findMembersWithFilter(Pageable pageable, FindMemberRequest f } @Override - public Optional findByNicknameOrEmail(String nickname, String email) { + public Optional findByNicknameAndEmail(String nickname, String email) { return memberRepository.findByNicknameAndEmail(nickname, email).map(memberPersistenceMapper::toDomain); } + + @Override + public Optional findByNameAndEmail(String name, String email) { + return memberRepository.findByNameAndEmail(name, email).map(memberPersistenceMapper::toDomain); + } } diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberRepository.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberRepository.java index eb573737..4f608bd9 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberRepository.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberRepository.java @@ -24,5 +24,7 @@ public interface MemberRepository extends JpaRepository, Me Optional findByMemberIdAndIsReviewerTrue(Long memberId); Optional findByNicknameAndEmail(String nickname, String email); + + Optional findByNameAndEmail(String name, String email); } diff --git a/src/main/java/clap/server/application/port/inbound/member/SendNewPasswordUsecase.java b/src/main/java/clap/server/application/port/inbound/member/SendNewPasswordUsecase.java new file mode 100644 index 00000000..cf515718 --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/member/SendNewPasswordUsecase.java @@ -0,0 +1,7 @@ +package clap.server.application.port.inbound.member; + +import clap.server.adapter.inbound.web.dto.member.request.SendInitialPasswordRequest; + +public interface SendNewPasswordUsecase { + void sendInitialPassword(SendInitialPasswordRequest request); +} diff --git a/src/main/java/clap/server/application/port/inbound/member/SendVerificationEmailUsecase.java b/src/main/java/clap/server/application/port/inbound/member/SendVerificationEmailUsecase.java index 1056b13f..5748520f 100644 --- a/src/main/java/clap/server/application/port/inbound/member/SendVerificationEmailUsecase.java +++ b/src/main/java/clap/server/application/port/inbound/member/SendVerificationEmailUsecase.java @@ -1,6 +1,6 @@ package clap.server.application.port.inbound.member; -import clap.server.adapter.inbound.web.dto.member.response.SendVerificationCodeRequest; +import clap.server.adapter.inbound.web.dto.member.request.SendVerificationCodeRequest; public interface SendVerificationEmailUsecase { void sendVerificationCode(SendVerificationCodeRequest request); diff --git a/src/main/java/clap/server/application/port/inbound/member/VerifyEmailCodeUsecase.java b/src/main/java/clap/server/application/port/inbound/member/VerifyEmailCodeUsecase.java index 290b3d98..fe0441a9 100644 --- a/src/main/java/clap/server/application/port/inbound/member/VerifyEmailCodeUsecase.java +++ b/src/main/java/clap/server/application/port/inbound/member/VerifyEmailCodeUsecase.java @@ -1,6 +1,6 @@ package clap.server.application.port.inbound.member; -import clap.server.adapter.inbound.web.dto.member.response.VerifyCodeRequest; +import clap.server.adapter.inbound.web.dto.member.request.VerifyCodeRequest; public interface VerifyEmailCodeUsecase { void verifyEmailCode(VerifyCodeRequest request); diff --git a/src/main/java/clap/server/application/port/outbound/email/SendEmailPort.java b/src/main/java/clap/server/application/port/outbound/email/SendEmailPort.java index 6cd8cf86..eedd1979 100644 --- a/src/main/java/clap/server/application/port/outbound/email/SendEmailPort.java +++ b/src/main/java/clap/server/application/port/outbound/email/SendEmailPort.java @@ -6,4 +6,6 @@ public interface SendEmailPort { void sendVerificationEmail(String memberEmail, String receiverName, String verificationCode); + void sendNewPasswordEmail(String memberEmail, String receiverName, String newPassword); + } \ No newline at end of file diff --git a/src/main/java/clap/server/application/port/outbound/member/LoadMemberPort.java b/src/main/java/clap/server/application/port/outbound/member/LoadMemberPort.java index e83a261b..972a5edd 100644 --- a/src/main/java/clap/server/application/port/outbound/member/LoadMemberPort.java +++ b/src/main/java/clap/server/application/port/outbound/member/LoadMemberPort.java @@ -21,8 +21,6 @@ public interface LoadMemberPort { List findTasksByMemberIdAndStatus(Long memberId, List taskStatuses); - int getRemainingTasks(Long memberId); - Optional findReviewerById(Long id); Optional findByNickname(String nickname); @@ -33,5 +31,7 @@ public interface LoadMemberPort { Page findMembersWithFilter(Pageable pageable, FindMemberRequest filterRequest, String sortDirection); - Optional findByNicknameOrEmail(String nickname, String email); + Optional findByNicknameAndEmail(String nickname, String email); + + Optional findByNameAndEmail(String name, String email); } diff --git a/src/main/java/clap/server/application/service/admin/SendInvitationService.java b/src/main/java/clap/server/application/service/admin/SendInvitationService.java index 9bdee671..b17918f1 100644 --- a/src/main/java/clap/server/application/service/admin/SendInvitationService.java +++ b/src/main/java/clap/server/application/service/admin/SendInvitationService.java @@ -31,7 +31,7 @@ public void sendInvitation(SendInvitationRequest request) { .orElseThrow(() -> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); // 초기 비밀번호 생성 - String initialPassword = passwordGenerator.generateRandomPassword(8); + String initialPassword = passwordGenerator.generateRandomPassword(); String encodedPassword = passwordEncoder.encode(initialPassword); // 회원 비밀번호 업데이트 diff --git a/src/main/java/clap/server/application/service/auth/EmailVerificationService.java b/src/main/java/clap/server/application/service/auth/EmailVerificationService.java index d0212053..f44934f6 100644 --- a/src/main/java/clap/server/application/service/auth/EmailVerificationService.java +++ b/src/main/java/clap/server/application/service/auth/EmailVerificationService.java @@ -1,8 +1,7 @@ package clap.server.application.service.auth; -import clap.server.adapter.inbound.web.dto.member.response.SendVerificationCodeRequest; -import clap.server.adapter.inbound.web.dto.member.response.VerifyCodeRequest; -import clap.server.application.port.inbound.domain.MemberService; +import clap.server.adapter.inbound.web.dto.member.request.SendVerificationCodeRequest; +import clap.server.adapter.inbound.web.dto.member.request.VerifyCodeRequest; import clap.server.application.port.inbound.member.SendVerificationEmailUsecase; import clap.server.application.port.inbound.member.VerifyEmailCodeUsecase; import clap.server.application.port.outbound.auth.otp.CommandOtpPort; @@ -22,7 +21,6 @@ @ApplicationService @RequiredArgsConstructor public class EmailVerificationService implements SendVerificationEmailUsecase, VerifyEmailCodeUsecase { - private final MemberService memberService; private final LoadMemberPort loadMemberPort; private final SendEmailPort sendEmailPort; private final CommandOtpPort commandOtpPort; @@ -31,7 +29,8 @@ public class EmailVerificationService implements SendVerificationEmailUsecase, V @Override @Transactional public void sendVerificationCode(SendVerificationCodeRequest request) { - Member member = loadMemberPort.findByNicknameOrEmail(request.nickname(), request.email()) + Member member = loadMemberPort. + findByNicknameAndEmail(request.nickname(), request.email()) .orElseThrow(() -> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); String verificationCode = VerificationCodeGenerator.generateRandomCode(); commandOtpPort.save(new Otp(member.getMemberInfo().getEmail(), verificationCode)); diff --git a/src/main/java/clap/server/application/service/member/ResetPasswordService.java b/src/main/java/clap/server/application/service/member/ResetPasswordService.java index 4baad9b5..59ce3e39 100644 --- a/src/main/java/clap/server/application/service/member/ResetPasswordService.java +++ b/src/main/java/clap/server/application/service/member/ResetPasswordService.java @@ -1,11 +1,18 @@ package clap.server.application.service.member; +import clap.server.adapter.inbound.web.dto.member.request.SendInitialPasswordRequest; import clap.server.application.port.inbound.domain.MemberService; import clap.server.application.port.inbound.member.ResetInitialPasswordUsecase; import clap.server.application.port.inbound.member.ResetPasswordUsecase; +import clap.server.application.port.inbound.member.SendNewPasswordUsecase; +import clap.server.application.port.outbound.email.SendEmailPort; import clap.server.application.port.outbound.member.CommandMemberPort; +import clap.server.application.port.outbound.member.LoadMemberPort; import clap.server.common.annotation.architecture.ApplicationService; +import clap.server.common.utils.InitialPasswordGenerator; import clap.server.domain.model.member.Member; +import clap.server.exception.ApplicationException; +import clap.server.exception.code.MemberErrorCode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.password.PasswordEncoder; @@ -15,10 +22,13 @@ @RequiredArgsConstructor @Transactional @Slf4j -class ResetPasswordService implements ResetPasswordUsecase, ResetInitialPasswordUsecase { +class ResetPasswordService implements ResetPasswordUsecase, ResetInitialPasswordUsecase, SendNewPasswordUsecase { private final MemberService memberService; private final PasswordEncoder passwordEncoder; private final CommandMemberPort commandMemberPort; + private final LoadMemberPort loadMemberPort; + private final InitialPasswordGenerator initialPasswordGenerator; + private final SendEmailPort sendEmailPort; @Override public void resetPassword(Long memberId, String inputPassword) { @@ -34,4 +44,17 @@ public void resetPasswordAndActivateMember(Long memberId, String password) { member.resetPasswordAndActivateMember(passwordEncoder.encode(password)); commandMemberPort.save(member); } + + @Override + public void sendInitialPassword(SendInitialPasswordRequest request) { + Member member = loadMemberPort.findByNameAndEmail(request.name(), request.email()) + .orElseThrow( + () -> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); + + String newPassword = initialPasswordGenerator.generateRandomPassword(); + sendEmailPort.sendNewPasswordEmail(request.email(), request.name(), newPassword); + String encodedPassword = passwordEncoder.encode(newPassword); + member.resetPassword(encodedPassword); + commandMemberPort.save(member); + } } diff --git a/src/main/java/clap/server/common/utils/InitialPasswordGenerator.java b/src/main/java/clap/server/common/utils/InitialPasswordGenerator.java index 3e5ed23f..6973d0d7 100644 --- a/src/main/java/clap/server/common/utils/InitialPasswordGenerator.java +++ b/src/main/java/clap/server/common/utils/InitialPasswordGenerator.java @@ -1,28 +1,39 @@ package clap.server.common.utils; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.security.SecureRandom; @Component public class InitialPasswordGenerator { - @Value("${password.policy.characters:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()}") - private String characters; + private static final String CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()"; + private static final int UPPER = 0, LOWER = 26, DIGIT = 52, SPECIAL = 62; + private static final int PASSWORD_LENGTH = 8; - public String generateRandomPassword(int length) { - if (length <= 0) { - throw new IllegalArgumentException("Password length must be greater than 0"); + private final SecureRandom random = new SecureRandom(); + + public String generateRandomPassword() { + char[] password = new char[PASSWORD_LENGTH]; + int[] cases = {UPPER, LOWER, DIGIT, SPECIAL}; + + for (int i = 0; i < 4; i++) { + int start = cases[i]; + int end = (i == 3) ? CHARS.length() : cases[i + 1]; + password[i] = CHARS.charAt(start + random.nextInt(end - start)); } - SecureRandom secureRandom = new SecureRandom(); - StringBuilder password = new StringBuilder(length); + for (int i = 4; i < PASSWORD_LENGTH; i++) { + password[i] = CHARS.charAt(random.nextInt(CHARS.length())); + } - for (int i = 0; i < length; i++) { - int randomIndex = secureRandom.nextInt(characters.length()); - password.append(characters.charAt(randomIndex)); + for (int i = PASSWORD_LENGTH - 1; i > 0; i--) { + int j = random.nextInt(i + 1); + char temp = password[i]; + password[i] = password[j]; + password[j] = temp; } - return password.toString(); + return new String(password); } } + diff --git a/src/main/resources/templates/new-password.html b/src/main/resources/templates/new-password.html new file mode 100644 index 00000000..b71275b9 --- /dev/null +++ b/src/main/resources/templates/new-password.html @@ -0,0 +1,88 @@ + + + + + [TaskFlow] 비밀번호 재설정 + + + + + +