Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
){
}
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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")
Expand All @@ -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);
}
}
17 changes: 17 additions & 0 deletions src/main/java/clap/server/adapter/outbound/api/EmailClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,6 @@ public List<Member> findActiveManagers() {
.collect(Collectors.toList());
}

@Override
public int getRemainingTasks(Long memberId) {
List<TaskStatus> targetStatuses = List.of(TaskStatus.IN_PROGRESS, TaskStatus.IN_REVIEWING);
return findTasksByMemberIdAndStatus(memberId, targetStatuses).size();
}

@Override
public List<Task> findTasksByMemberIdAndStatus(Long memberId, List<TaskStatus> taskStatuses) {
List<TaskEntity> taskEntities = taskRepository.findByProcessor_MemberIdAndTaskStatusIn(memberId, taskStatuses);
Expand All @@ -111,8 +105,13 @@ public Page<Member> findMembersWithFilter(Pageable pageable, FindMemberRequest f
}

@Override
public Optional<Member> findByNicknameOrEmail(String nickname, String email) {
public Optional<Member> findByNicknameAndEmail(String nickname, String email) {
return memberRepository.findByNicknameAndEmail(nickname, email).map(memberPersistenceMapper::toDomain);
}

@Override
public Optional<Member> findByNameAndEmail(String name, String email) {
return memberRepository.findByNameAndEmail(name, email).map(memberPersistenceMapper::toDomain);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,7 @@ public interface MemberRepository extends JpaRepository<MemberEntity, Long>, Me
Optional<MemberEntity> findByMemberIdAndIsReviewerTrue(Long memberId);

Optional<MemberEntity> findByNicknameAndEmail(String nickname, String email);

Optional<MemberEntity> findByNameAndEmail(String name, String email);
}

Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ public interface SendEmailPort {

void sendVerificationEmail(String memberEmail, String receiverName, String verificationCode);

void sendNewPasswordEmail(String memberEmail, String receiverName, String newPassword);

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ public interface LoadMemberPort {

List<Task> findTasksByMemberIdAndStatus(Long memberId, List<TaskStatus> taskStatuses);

int getRemainingTasks(Long memberId);

Optional<Member> findReviewerById(Long id);

Optional<Member> findByNickname(String nickname);
Expand All @@ -33,5 +31,7 @@ public interface LoadMemberPort {

Page<Member> findMembersWithFilter(Pageable pageable, FindMemberRequest filterRequest, String sortDirection);

Optional<Member> findByNicknameOrEmail(String nickname, String email);
Optional<Member> findByNicknameAndEmail(String nickname, String email);

Optional<Member> findByNameAndEmail(String name, String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);

// 회원 비밀번호 업데이트
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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) {
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}


Loading