Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import clap.server.adapter.outbound.jwt.JwtClaims;
import clap.server.adapter.outbound.jwt.access.AccessTokenClaimKeys;
import clap.server.application.port.outbound.auth.ForbiddenTokenPort;
import clap.server.application.port.outbound.auth.forbidden.ForbiddenTokenPort;
import clap.server.application.port.outbound.auth.JwtProvider;
import clap.server.exception.JwtException;
import clap.server.exception.code.AuthErrorCode;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package clap.server.adapter.inbound.web.member;

import clap.server.adapter.inbound.security.service.SecurityUserDetails;
import clap.server.application.port.inbound.member.SendVerificationEmailUsecase;
import clap.server.application.port.inbound.member.VerifyEmailCodeUsecase;
import clap.server.common.annotation.architecture.WebAdapter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Tag(name = "00. Auth [인증번호]")
@WebAdapter
@RequiredArgsConstructor
@RequestMapping("/api/members")
public class EmailVerificationController {
private final SendVerificationEmailUsecase sendVerificationEmailUsecase;
private final VerifyEmailCodeUsecase verifyEmailCodeUsecase;

@Operation(summary = "인증번호 전송 API")
@PostMapping("/verification/email")
public void sendVerificationEmail(@AuthenticationPrincipal SecurityUserDetails userInfo){
sendVerificationEmailUsecase.sendVerificationCode(userInfo.getUserId());
}

@Operation(summary = "인증번호 검증 API")
@PostMapping("/verification")
public void sendVerificationEmail(@AuthenticationPrincipal SecurityUserDetails userInfo,
@RequestParam String code){
verifyEmailCodeUsecase.verifyEmailCode(userInfo.getUserId(), code);
}
}
24 changes: 22 additions & 2 deletions src/main/java/clap/server/adapter/outbound/api/EmailClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import clap.server.adapter.outbound.api.dto.EmailTemplate;
import clap.server.adapter.outbound.api.dto.PushNotificationTemplate;
import clap.server.application.port.outbound.webhook.SendEmailPort;
import clap.server.application.port.outbound.email.SendEmailPort;
import clap.server.application.port.outbound.webhook.SendWebhookEmailPort;
import clap.server.common.annotation.architecture.ExternalApiAdapter;
import clap.server.exception.AdapterException;
import clap.server.exception.code.NotificationErrorCode;
Expand All @@ -13,7 +14,7 @@

@ExternalApiAdapter
@RequiredArgsConstructor
public class EmailClient implements SendEmailPort {
public class EmailClient implements SendEmailPort, SendWebhookEmailPort {

private final EmailTemplateBuilder emailTemplateBuilder;
private final JavaMailSender mailSender;
Expand Down Expand Up @@ -51,4 +52,23 @@ public void sendInvitationEmail(String memberEmail, String receiverName, String
throw new AdapterException(NotificationErrorCode.EMAIL_SEND_FAILED);
}
}

@Override
public void sendVerificationEmail(String memberEmail, String receiverName, String verificationCode) {
try {
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");

EmailTemplate template = emailTemplateBuilder.createVerificationCodeTemplate(memberEmail, receiverName, verificationCode);
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 @@ -69,4 +69,14 @@ public EmailTemplate createInvitationTemplate(String receiver, String receiverNa
String body = templateEngine.process(templateName, context);
return new EmailTemplate(receiver, subject, body);
}

public EmailTemplate createVerificationCodeTemplate(String receiver, String receiverName, String verificationCode) {
Context context = new Context();
String templateName = "verification";
String subject = "[TaskFlow] 비밀번호 재설정 인증 번호";
context.setVariable("verificationCode", verificationCode);
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
@@ -1,6 +1,6 @@
package clap.server.adapter.outbound.infrastructure.redis.forbidden;

import clap.server.application.port.outbound.auth.ForbiddenTokenPort;
import clap.server.application.port.outbound.auth.forbidden.ForbiddenTokenPort;
import clap.server.common.annotation.architecture.InfrastructureAdapter;
import clap.server.domain.model.auth.ForbiddenToken;
import lombok.RequiredArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package clap.server.adapter.outbound.infrastructure.redis.log;

import clap.server.application.port.outbound.auth.CommandLoginLogPort;
import clap.server.application.port.outbound.auth.LoadLoginLogPort;
import clap.server.application.port.outbound.auth.loginLog.CommandLoginLogPort;
import clap.server.application.port.outbound.auth.loginLog.LoadLoginLogPort;
import clap.server.common.annotation.architecture.InfrastructureAdapter;
import clap.server.domain.model.auth.LoginLog;
import lombok.RequiredArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package clap.server.adapter.outbound.infrastructure.redis.otp;

import clap.server.application.port.outbound.auth.otp.CommandOtpPort;
import clap.server.application.port.outbound.auth.otp.LoadOtpPort;
import clap.server.common.annotation.architecture.InfrastructureAdapter;
import clap.server.domain.model.auth.Otp;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.util.Optional;

@Slf4j
@InfrastructureAdapter
@RequiredArgsConstructor
public class OtpAdapter implements LoadOtpPort, CommandOtpPort {
private final OtpRepository otpRepository;
private final OtpMapper otpMapper;

@Override
public void save(Otp otp) {
OtpEntity refreshTokenEntity = otpMapper.toEntity(otp);
otpRepository.save(refreshTokenEntity);
}

@Override
public void deleteByEmail(String email) {
otpRepository.deleteById(email);
}

@Override
public Optional<Otp> findByEmail(String email) {
return otpRepository.findById(email).map(otpMapper::toDomain);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package clap.server.adapter.outbound.infrastructure.redis.otp;

import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

@RedisHash(value = "OTP", timeToLive = 300) // 300초(5분) 후 자동 삭제
@Getter
@Builder
@ToString(of = {"email", "code"})
public class OtpEntity {
@Id
private String email;
private String code;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package clap.server.adapter.outbound.infrastructure.redis.otp;

import clap.server.domain.model.auth.Otp;
import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring")
public interface OtpMapper {
@InheritInverseConfiguration
Otp toDomain(final OtpEntity entity);

OtpEntity toEntity(final Otp domain);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package clap.server.adapter.outbound.infrastructure.redis.otp;

import org.springframework.data.repository.CrudRepository;

public interface OtpRepository extends CrudRepository<OtpEntity, String>{
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package clap.server.adapter.outbound.infrastructure.redis.refresh;

import clap.server.application.port.outbound.auth.CommandRefreshTokenPort;
import clap.server.application.port.outbound.auth.LoadRefreshTokenPort;
import clap.server.application.port.outbound.auth.refresh.CommandRefreshTokenPort;
import clap.server.application.port.outbound.auth.refresh.LoadRefreshTokenPort;
import clap.server.common.annotation.architecture.InfrastructureAdapter;
import clap.server.domain.model.auth.RefreshToken;
import lombok.RequiredArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package clap.server.application.port.inbound.domain;

import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus;
import clap.server.application.port.outbound.auth.LoadLoginLogPort;
import clap.server.application.port.outbound.auth.loginLog.LoadLoginLogPort;
import clap.server.application.port.outbound.log.CommandLogPort;
import clap.server.common.utils.ClientIpParseUtil;
import clap.server.domain.model.auth.LoginLog;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package clap.server.application.port.inbound.member;

public interface SendVerificationEmailUsecase {
void sendVerificationCode(Long memberId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package clap.server.application.port.inbound.member;

public interface VerifyEmailCodeUsecase {
void verifyEmailCode(Long memberId, String code);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package clap.server.application.port.inbound.member;

public interface VerifyPasswordUseCase {
void verifyPassword(Long memberId, String password);
void verifyPassword(Long memberId, String inputPassword);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package clap.server.application.port.outbound.auth;
package clap.server.application.port.outbound.auth.forbidden;

import clap.server.domain.model.auth.ForbiddenToken;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package clap.server.application.port.outbound.auth;
package clap.server.application.port.outbound.auth.loginLog;

import clap.server.domain.model.auth.LoginLog;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package clap.server.application.port.outbound.auth;
package clap.server.application.port.outbound.auth.loginLog;

import clap.server.domain.model.auth.LoginLog;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package clap.server.application.port.outbound.auth.otp;

import clap.server.domain.model.auth.Otp;

public interface CommandOtpPort {
void save(Otp otp);
void deleteByEmail(String email);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package clap.server.application.port.outbound.auth.otp;

import clap.server.domain.model.auth.Otp;

import java.util.Optional;

public interface LoadOtpPort {
Optional<Otp> findByEmail(String email);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package clap.server.application.port.outbound.auth;
package clap.server.application.port.outbound.auth.refresh;

import clap.server.domain.model.auth.RefreshToken;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package clap.server.application.port.outbound.auth;
package clap.server.application.port.outbound.auth.refresh;

import clap.server.domain.model.auth.RefreshToken;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package clap.server.application.port.outbound.email;

public interface SendEmailPort {

void sendInvitationEmail(String memberEmail, String receiverName, String initialPassword);

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@

import clap.server.adapter.outbound.api.dto.PushNotificationTemplate;

public interface SendEmailPort {
public interface SendWebhookEmailPort {

void sendWebhookEmail(PushNotificationTemplate request);

void sendInvitationEmail(String memberEmail, String receiverName, String initialPassword);

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import clap.server.application.port.inbound.admin.SendInvitationUsecase;
import clap.server.application.port.outbound.member.CommandMemberPort;
import clap.server.application.port.outbound.member.LoadMemberPort;
import clap.server.application.port.outbound.webhook.SendEmailPort;
import clap.server.application.port.outbound.email.SendEmailPort;
import clap.server.common.annotation.architecture.ApplicationService;
import clap.server.common.utils.InitialPasswordGenerator;
import clap.server.domain.model.member.Member;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import clap.server.application.mapper.AuthResponseMapper;
import clap.server.application.port.inbound.auth.LoginUsecase;
import clap.server.application.port.inbound.auth.LogoutUsecase;
import clap.server.application.port.outbound.auth.ForbiddenTokenPort;
import clap.server.application.port.outbound.auth.forbidden.ForbiddenTokenPort;
import clap.server.application.port.outbound.member.LoadMemberPort;
import clap.server.common.annotation.architecture.ApplicationService;
import clap.server.domain.model.auth.CustomJwts;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package clap.server.application.service.auth;

import clap.server.application.port.inbound.domain.MemberService;
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;
import clap.server.application.port.outbound.auth.otp.LoadOtpPort;
import clap.server.application.port.outbound.email.SendEmailPort;
import clap.server.common.annotation.architecture.ApplicationService;
import clap.server.common.utils.VerificationCodeGenerator;
import clap.server.domain.model.auth.Otp;
import clap.server.domain.model.member.Member;
import clap.server.exception.ApplicationException;
import clap.server.exception.code.AuthErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.transaction.annotation.Transactional;

@ApplicationService
@RequiredArgsConstructor
public class EmailVerificationService implements SendVerificationEmailUsecase, VerifyEmailCodeUsecase {
private final MemberService memberService;
private final SendEmailPort sendEmailPort;
private final CommandOtpPort commandOtpPort;
private final LoadOtpPort loadOtpPort;

@Override
@Transactional
public void sendVerificationCode(Long memberId) {
Member member = memberService.findActiveMember(memberId);
String verificationCode = VerificationCodeGenerator.generateRandomCode();
commandOtpPort.save(new Otp(member.getMemberInfo().getEmail(), verificationCode));
sendEmailPort.sendVerificationEmail(member.getMemberInfo().getEmail(), member.getNickname(), verificationCode);
}


@Override
@Transactional
public void verifyEmailCode(Long memberId, String code) {
Member member = memberService.findActiveMember(memberId);
Otp otp = loadOtpPort.findByEmail(member.getMemberInfo().getEmail()).orElseThrow(
() -> new ApplicationException(AuthErrorCode.VERIFICATION_CODE_EXPIRED)
);

if(!code.equals(otp.code())){
throw new ApplicationException(AuthErrorCode.VERIFICATION_CODE_MISMATCH);
}
else {
commandOtpPort.deleteByEmail(member.getMemberInfo().getEmail());
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package clap.server.application.service.auth;

import clap.server.application.port.outbound.auth.CommandLoginLogPort;
import clap.server.application.port.outbound.auth.LoadLoginLogPort;
import clap.server.application.port.outbound.auth.loginLog.CommandLoginLogPort;
import clap.server.application.port.outbound.auth.loginLog.LoadLoginLogPort;
import clap.server.domain.model.auth.LoginLog;
import clap.server.exception.AuthException;
import clap.server.exception.code.AuthErrorCode;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package clap.server.application.service.auth;

import clap.server.application.port.outbound.auth.CommandRefreshTokenPort;
import clap.server.application.port.outbound.auth.LoadRefreshTokenPort;
import clap.server.application.port.outbound.auth.refresh.CommandRefreshTokenPort;
import clap.server.application.port.outbound.auth.refresh.LoadRefreshTokenPort;
import clap.server.domain.model.auth.RefreshToken;
import clap.server.exception.AuthException;
import clap.server.exception.code.AuthErrorCode;
Expand Down
Loading