diff --git a/src/main/java/clap/server/adapter/inbound/security/filter/JwtAuthenticationFilter.java b/src/main/java/clap/server/adapter/inbound/security/filter/JwtAuthenticationFilter.java index aad8b990..a55121eb 100644 --- a/src/main/java/clap/server/adapter/inbound/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/clap/server/adapter/inbound/security/filter/JwtAuthenticationFilter.java @@ -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; diff --git a/src/main/java/clap/server/adapter/inbound/web/member/EmailVerificationController.java b/src/main/java/clap/server/adapter/inbound/web/member/EmailVerificationController.java new file mode 100644 index 00000000..c7c2efd9 --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/member/EmailVerificationController.java @@ -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); + } +} 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 1a93445f..c90b26d5 100644 --- a/src/main/java/clap/server/adapter/outbound/api/EmailClient.java +++ b/src/main/java/clap/server/adapter/outbound/api/EmailClient.java @@ -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; @@ -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; @@ -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); + } + } + + } 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 32451b2f..be20f02b 100644 --- a/src/main/java/clap/server/adapter/outbound/api/EmailTemplateBuilder.java +++ b/src/main/java/clap/server/adapter/outbound/api/EmailTemplateBuilder.java @@ -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); + } } diff --git a/src/main/java/clap/server/adapter/outbound/infrastructure/redis/forbidden/ForbiddenTokenAdapter.java b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/forbidden/ForbiddenTokenAdapter.java index d2831229..35509699 100644 --- a/src/main/java/clap/server/adapter/outbound/infrastructure/redis/forbidden/ForbiddenTokenAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/forbidden/ForbiddenTokenAdapter.java @@ -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; diff --git a/src/main/java/clap/server/adapter/outbound/infrastructure/redis/log/LoginLogAdapter.java b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/log/LoginLogAdapter.java index 3fd24e89..e747ec65 100644 --- a/src/main/java/clap/server/adapter/outbound/infrastructure/redis/log/LoginLogAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/log/LoginLogAdapter.java @@ -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; diff --git a/src/main/java/clap/server/adapter/outbound/infrastructure/redis/otp/OtpAdapter.java b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/otp/OtpAdapter.java new file mode 100644 index 00000000..7007f808 --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/otp/OtpAdapter.java @@ -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 findByEmail(String email) { + return otpRepository.findById(email).map(otpMapper::toDomain); + } +} diff --git a/src/main/java/clap/server/adapter/outbound/infrastructure/redis/otp/OtpEntity.java b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/otp/OtpEntity.java new file mode 100644 index 00000000..647f9e8b --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/otp/OtpEntity.java @@ -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; +} diff --git a/src/main/java/clap/server/adapter/outbound/infrastructure/redis/otp/OtpMapper.java b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/otp/OtpMapper.java new file mode 100644 index 00000000..73befd4b --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/otp/OtpMapper.java @@ -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); +} \ No newline at end of file diff --git a/src/main/java/clap/server/adapter/outbound/infrastructure/redis/otp/OtpRepository.java b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/otp/OtpRepository.java new file mode 100644 index 00000000..6f90fcc5 --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/otp/OtpRepository.java @@ -0,0 +1,6 @@ +package clap.server.adapter.outbound.infrastructure.redis.otp; + +import org.springframework.data.repository.CrudRepository; + +public interface OtpRepository extends CrudRepository{ +} diff --git a/src/main/java/clap/server/adapter/outbound/infrastructure/redis/refresh/RefreshTokenAdapter.java b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/refresh/RefreshTokenAdapter.java index 7c91371f..024307ba 100644 --- a/src/main/java/clap/server/adapter/outbound/infrastructure/redis/refresh/RefreshTokenAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/refresh/RefreshTokenAdapter.java @@ -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; diff --git a/src/main/java/clap/server/application/port/inbound/domain/LogService.java b/src/main/java/clap/server/application/port/inbound/domain/LogService.java index 05077cd9..8071143a 100644 --- a/src/main/java/clap/server/application/port/inbound/domain/LogService.java +++ b/src/main/java/clap/server/application/port/inbound/domain/LogService.java @@ -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; 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 new file mode 100644 index 00000000..8a7a3610 --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/member/SendVerificationEmailUsecase.java @@ -0,0 +1,5 @@ +package clap.server.application.port.inbound.member; + +public interface SendVerificationEmailUsecase { + void sendVerificationCode(Long memberId); +} 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 new file mode 100644 index 00000000..d2cf04c8 --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/member/VerifyEmailCodeUsecase.java @@ -0,0 +1,5 @@ +package clap.server.application.port.inbound.member; + +public interface VerifyEmailCodeUsecase { + void verifyEmailCode(Long memberId, String code); +} diff --git a/src/main/java/clap/server/application/port/inbound/member/VerifyPasswordUseCase.java b/src/main/java/clap/server/application/port/inbound/member/VerifyPasswordUseCase.java index 7544bfb2..fed3a030 100644 --- a/src/main/java/clap/server/application/port/inbound/member/VerifyPasswordUseCase.java +++ b/src/main/java/clap/server/application/port/inbound/member/VerifyPasswordUseCase.java @@ -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); } diff --git a/src/main/java/clap/server/application/port/outbound/auth/ForbiddenTokenPort.java b/src/main/java/clap/server/application/port/outbound/auth/forbidden/ForbiddenTokenPort.java similarity index 75% rename from src/main/java/clap/server/application/port/outbound/auth/ForbiddenTokenPort.java rename to src/main/java/clap/server/application/port/outbound/auth/forbidden/ForbiddenTokenPort.java index 1e73bf83..6020216a 100644 --- a/src/main/java/clap/server/application/port/outbound/auth/ForbiddenTokenPort.java +++ b/src/main/java/clap/server/application/port/outbound/auth/forbidden/ForbiddenTokenPort.java @@ -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; diff --git a/src/main/java/clap/server/application/port/outbound/auth/CommandLoginLogPort.java b/src/main/java/clap/server/application/port/outbound/auth/loginLog/CommandLoginLogPort.java similarity index 72% rename from src/main/java/clap/server/application/port/outbound/auth/CommandLoginLogPort.java rename to src/main/java/clap/server/application/port/outbound/auth/loginLog/CommandLoginLogPort.java index 5101078d..a2a6f16d 100644 --- a/src/main/java/clap/server/application/port/outbound/auth/CommandLoginLogPort.java +++ b/src/main/java/clap/server/application/port/outbound/auth/loginLog/CommandLoginLogPort.java @@ -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; diff --git a/src/main/java/clap/server/application/port/outbound/auth/LoadLoginLogPort.java b/src/main/java/clap/server/application/port/outbound/auth/loginLog/LoadLoginLogPort.java similarity index 73% rename from src/main/java/clap/server/application/port/outbound/auth/LoadLoginLogPort.java rename to src/main/java/clap/server/application/port/outbound/auth/loginLog/LoadLoginLogPort.java index 93bb4b59..66c73d42 100644 --- a/src/main/java/clap/server/application/port/outbound/auth/LoadLoginLogPort.java +++ b/src/main/java/clap/server/application/port/outbound/auth/loginLog/LoadLoginLogPort.java @@ -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; diff --git a/src/main/java/clap/server/application/port/outbound/auth/otp/CommandOtpPort.java b/src/main/java/clap/server/application/port/outbound/auth/otp/CommandOtpPort.java new file mode 100644 index 00000000..71edf927 --- /dev/null +++ b/src/main/java/clap/server/application/port/outbound/auth/otp/CommandOtpPort.java @@ -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); +} diff --git a/src/main/java/clap/server/application/port/outbound/auth/otp/LoadOtpPort.java b/src/main/java/clap/server/application/port/outbound/auth/otp/LoadOtpPort.java new file mode 100644 index 00000000..aca4f225 --- /dev/null +++ b/src/main/java/clap/server/application/port/outbound/auth/otp/LoadOtpPort.java @@ -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 findByEmail(String email); +} diff --git a/src/main/java/clap/server/application/port/outbound/auth/CommandRefreshTokenPort.java b/src/main/java/clap/server/application/port/outbound/auth/refresh/CommandRefreshTokenPort.java similarity index 75% rename from src/main/java/clap/server/application/port/outbound/auth/CommandRefreshTokenPort.java rename to src/main/java/clap/server/application/port/outbound/auth/refresh/CommandRefreshTokenPort.java index 61ffe0a6..f2e16265 100644 --- a/src/main/java/clap/server/application/port/outbound/auth/CommandRefreshTokenPort.java +++ b/src/main/java/clap/server/application/port/outbound/auth/refresh/CommandRefreshTokenPort.java @@ -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; diff --git a/src/main/java/clap/server/application/port/outbound/auth/LoadRefreshTokenPort.java b/src/main/java/clap/server/application/port/outbound/auth/refresh/LoadRefreshTokenPort.java similarity index 75% rename from src/main/java/clap/server/application/port/outbound/auth/LoadRefreshTokenPort.java rename to src/main/java/clap/server/application/port/outbound/auth/refresh/LoadRefreshTokenPort.java index 1fe8f5e9..20521055 100644 --- a/src/main/java/clap/server/application/port/outbound/auth/LoadRefreshTokenPort.java +++ b/src/main/java/clap/server/application/port/outbound/auth/refresh/LoadRefreshTokenPort.java @@ -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; 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 new file mode 100644 index 00000000..7375cbc8 --- /dev/null +++ b/src/main/java/clap/server/application/port/outbound/email/SendEmailPort.java @@ -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); + +} \ No newline at end of file diff --git a/src/main/java/clap/server/application/port/outbound/webhook/SendEmailPort.java b/src/main/java/clap/server/application/port/outbound/webhook/SendWebhookEmailPort.java similarity index 59% rename from src/main/java/clap/server/application/port/outbound/webhook/SendEmailPort.java rename to src/main/java/clap/server/application/port/outbound/webhook/SendWebhookEmailPort.java index 33a99aee..598926b4 100644 --- a/src/main/java/clap/server/application/port/outbound/webhook/SendEmailPort.java +++ b/src/main/java/clap/server/application/port/outbound/webhook/SendWebhookEmailPort.java @@ -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); - } \ No newline at end of file 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 b15b221c..4002ee34 100644 --- a/src/main/java/clap/server/application/service/admin/SendInvitationService.java +++ b/src/main/java/clap/server/application/service/admin/SendInvitationService.java @@ -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; diff --git a/src/main/java/clap/server/application/service/auth/AuthService.java b/src/main/java/clap/server/application/service/auth/AuthService.java index af265d8f..5d5e59f7 100644 --- a/src/main/java/clap/server/application/service/auth/AuthService.java +++ b/src/main/java/clap/server/application/service/auth/AuthService.java @@ -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; diff --git a/src/main/java/clap/server/application/service/auth/EmailVerificationService.java b/src/main/java/clap/server/application/service/auth/EmailVerificationService.java new file mode 100644 index 00000000..8c062a91 --- /dev/null +++ b/src/main/java/clap/server/application/service/auth/EmailVerificationService.java @@ -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()); + } + } +} diff --git a/src/main/java/clap/server/application/service/auth/LoginAttemptService.java b/src/main/java/clap/server/application/service/auth/LoginAttemptService.java index a205ae9d..ca53f242 100644 --- a/src/main/java/clap/server/application/service/auth/LoginAttemptService.java +++ b/src/main/java/clap/server/application/service/auth/LoginAttemptService.java @@ -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; diff --git a/src/main/java/clap/server/application/service/auth/RefreshTokenService.java b/src/main/java/clap/server/application/service/auth/RefreshTokenService.java index ecec64a3..4fae3ee7 100644 --- a/src/main/java/clap/server/application/service/auth/RefreshTokenService.java +++ b/src/main/java/clap/server/application/service/auth/RefreshTokenService.java @@ -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; diff --git a/src/main/java/clap/server/application/service/auth/ReissueTokenService.java b/src/main/java/clap/server/application/service/auth/ReissueTokenService.java index 568b9705..904c6ec3 100644 --- a/src/main/java/clap/server/application/service/auth/ReissueTokenService.java +++ b/src/main/java/clap/server/application/service/auth/ReissueTokenService.java @@ -2,7 +2,7 @@ import clap.server.adapter.inbound.web.dto.auth.response.ReissueTokenResponse; import clap.server.application.port.inbound.auth.ReissueTokenUsecase; -import clap.server.application.port.outbound.auth.CommandRefreshTokenPort; +import clap.server.application.port.outbound.auth.refresh.CommandRefreshTokenPort; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.domain.model.auth.CustomJwts; import clap.server.domain.model.auth.RefreshToken; diff --git a/src/main/java/clap/server/application/service/member/VerifyPasswordService.java b/src/main/java/clap/server/application/service/member/VerifyPasswordService.java index 95a13341..0833f323 100644 --- a/src/main/java/clap/server/application/service/member/VerifyPasswordService.java +++ b/src/main/java/clap/server/application/service/member/VerifyPasswordService.java @@ -4,10 +4,16 @@ import clap.server.application.port.inbound.member.VerifyPasswordUseCase; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.domain.model.member.Member; +import clap.server.exception.ApplicationException; +import clap.server.exception.AuthException; +import clap.server.exception.code.AuthErrorCode; +import clap.server.exception.code.MemberErrorCode; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.transaction.annotation.Transactional; +@Slf4j @ApplicationService @RequiredArgsConstructor @Transactional(readOnly = true) @@ -16,9 +22,10 @@ class VerifyPasswordService implements VerifyPasswordUseCase { private final PasswordEncoder passwordEncoder; @Override - public void verifyPassword(Long memberId, String password) { + public void verifyPassword(Long memberId, String inputPassword) { Member member = memberService.findActiveMember(memberId); - String encodedPassword = passwordEncoder.encode(password); - member.verifyPassword(encodedPassword); + if (!passwordEncoder.matches(member.getPassword(), inputPassword)) { + throw new ApplicationException(MemberErrorCode.PASSWORD_VERIFY_FAILED); + } } } diff --git a/src/main/java/clap/server/application/service/webhook/SendNotificationService.java b/src/main/java/clap/server/application/service/webhook/SendNotificationService.java index 7e12f07b..169f40c1 100644 --- a/src/main/java/clap/server/application/service/webhook/SendNotificationService.java +++ b/src/main/java/clap/server/application/service/webhook/SendNotificationService.java @@ -20,7 +20,7 @@ public class SendNotificationService { private final SendSseService sendSseService; private final SendAgitService sendAgitService; - private final SendEmailService sendEmailService; + private final SendWebhookEmailService sendWebhookEmailService; private final SendKaKaoWorkService sendKaKaoWorkService; private final CommandNotificationPort commandNotificationPort; @@ -54,7 +54,7 @@ public void sendPushNotification(Member receiver, NotificationType notificationT CompletableFuture sendEmailFuture = CompletableFuture.runAsync(() -> { if (receiver.getEmailNotificationEnabled()) { - sendEmailService.sendEmail(pushNotificationTemplate); + sendWebhookEmailService.sendEmail(pushNotificationTemplate); } }); diff --git a/src/main/java/clap/server/application/service/webhook/SendEmailService.java b/src/main/java/clap/server/application/service/webhook/SendWebhookEmailService.java similarity index 60% rename from src/main/java/clap/server/application/service/webhook/SendEmailService.java rename to src/main/java/clap/server/application/service/webhook/SendWebhookEmailService.java index b7a0f645..1ea80790 100644 --- a/src/main/java/clap/server/application/service/webhook/SendEmailService.java +++ b/src/main/java/clap/server/application/service/webhook/SendWebhookEmailService.java @@ -1,17 +1,17 @@ package clap.server.application.service.webhook; import clap.server.adapter.outbound.api.dto.PushNotificationTemplate; -import clap.server.application.port.outbound.webhook.SendEmailPort; +import clap.server.application.port.outbound.webhook.SendWebhookEmailPort; import clap.server.common.annotation.architecture.ApplicationService; import lombok.RequiredArgsConstructor; @ApplicationService @RequiredArgsConstructor -public class SendEmailService { +public class SendWebhookEmailService { - private final SendEmailPort port; + private final SendWebhookEmailPort sendWebhookEmailPort; public void sendEmail(PushNotificationTemplate request) { - port.sendWebhookEmail(request); + sendWebhookEmailPort.sendWebhookEmail(request); } } diff --git a/src/main/java/clap/server/common/utils/VerificationCodeGenerator.java b/src/main/java/clap/server/common/utils/VerificationCodeGenerator.java new file mode 100644 index 00000000..94e159e7 --- /dev/null +++ b/src/main/java/clap/server/common/utils/VerificationCodeGenerator.java @@ -0,0 +1,17 @@ +package clap.server.common.utils; + +import java.util.Random; + +public class VerificationCodeGenerator { + + public static String generateRandomCode() { + Random random = new Random(); + StringBuilder code = new StringBuilder(); + + for (int i = 0; i < 6; i++) { + code.append(random.nextInt(10)); + } + + return code.toString(); + } +} diff --git a/src/main/java/clap/server/config/security/SecurityFilterConfig.java b/src/main/java/clap/server/config/security/SecurityFilterConfig.java index b93176d3..e3994c5a 100644 --- a/src/main/java/clap/server/config/security/SecurityFilterConfig.java +++ b/src/main/java/clap/server/config/security/SecurityFilterConfig.java @@ -3,7 +3,7 @@ import clap.server.adapter.inbound.security.filter.LoginAttemptFilter; import clap.server.adapter.inbound.security.filter.JwtAuthenticationFilter; import clap.server.adapter.inbound.security.filter.JwtExceptionFilter; -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.application.service.auth.LoginAttemptService; import lombok.AccessLevel; diff --git a/src/main/java/clap/server/domain/model/auth/Otp.java b/src/main/java/clap/server/domain/model/auth/Otp.java new file mode 100644 index 00000000..c001b819 --- /dev/null +++ b/src/main/java/clap/server/domain/model/auth/Otp.java @@ -0,0 +1,7 @@ +package clap.server.domain.model.auth; + +public record Otp( + String email, + String code +) { +} diff --git a/src/main/java/clap/server/exception/code/AuthErrorCode.java b/src/main/java/clap/server/exception/code/AuthErrorCode.java index 6ca63b69..9ca50b1f 100644 --- a/src/main/java/clap/server/exception/code/AuthErrorCode.java +++ b/src/main/java/clap/server/exception/code/AuthErrorCode.java @@ -24,6 +24,8 @@ public enum AuthErrorCode implements BaseErrorCode { ACCOUNT_IS_LOCKED(HttpStatus.UNAUTHORIZED, "AUTH_015", "접근할 수 없는 계정입니다."), LOGIN_REQUEST_FAILED(HttpStatus.UNAUTHORIZED, "AUTH_016", "로그인에 실패하였습니다."), REFRESH_TOKEN_MISMATCHED(HttpStatus.UNAUTHORIZED, "AUTH_017", "리프레시 토큰이 일치하지 않습니다"), + VERIFICATION_CODE_MISMATCH(HttpStatus.BAD_REQUEST, "AUTH_018", "인증번호가 일치하지 않습니다."), + VERIFICATION_CODE_EXPIRED(HttpStatus.BAD_REQUEST, "AUTH_019", "만료된 인증번호입니다.") ; private final HttpStatus httpStatus; diff --git a/src/main/resources/notifications.yml b/src/main/resources/notifications.yml index e4ef655e..a64ec559 100644 --- a/src/main/resources/notifications.yml +++ b/src/main/resources/notifications.yml @@ -1,4 +1,3 @@ -#TODO 구글 알림 이메일 발신자 정보 설정(논의 필요) spring: mail: host: smtp.gmail.com diff --git a/src/main/resources/templates/verification.html b/src/main/resources/templates/verification.html new file mode 100644 index 00000000..1be601e9 --- /dev/null +++ b/src/main/resources/templates/verification.html @@ -0,0 +1,85 @@ + + + + + [TaskFlow] 비밀번호 재설정 인증 번호 <span th:text="${verificationCode}">인증번호</span> + + + + + +