-
Notifications
You must be signed in to change notification settings - Fork 0
✨ 이메일 인증 로직 #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
✨ 이메일 인증 로직 #29
Changes from all commits
d08c366
c8ed42a
e04cd81
3c6fed1
ac538e0
b16a0bf
2cdd23f
9b9d3dd
663a7f2
bafa9c5
0b96396
daed30e
12a3d87
9a4a6f7
4d7cef5
a20668b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 와, 이걸 이렇게 써버리네요 ㅋㅋㅋㅋㅋㅋ 재밌는 거 배워갑니다. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| package io.oopy.coding.common.constant; | ||
|
|
||
| import lombok.AccessLevel; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @NoArgsConstructor(access = AccessLevel.PRIVATE) | ||
| public class EmailConstant { | ||
| public static final Integer EMAIL_EXPIRED_PERIOD_SECONDS = 600; // 임시 10분 | ||
| public static final String CERT_TITLE = String.format("[%s] 조직 이메일 인증", GlobalConstant.PROJECT_NAME); | ||
| public static final String CERT_CONTENT = "<head>\n" + | ||
| "<meta charset=\"UTF-8\">\n" + | ||
| "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" + | ||
| "<style>\n" + | ||
| " .link-button {\n" + | ||
| " display: inline-block;\n" + | ||
| " padding: 10px 20px;\n" + | ||
| " background-color: #3498db;\n" + | ||
| " color: #fff;\n" + | ||
| " text-align: center;\n" + | ||
| " text-decoration: none;\n" + | ||
| " border-radius: 5px;\n" + | ||
| " border: none;\n" + | ||
| " cursor: pointer;\n" + | ||
| " transition: background-color 0.3s;\n" + | ||
| " cursor: pointer\n"+ | ||
| " }\n" + | ||
| " .text-color {\n" + | ||
| " color: #fff;\n" + | ||
| " }\n" + | ||
| "\n" + | ||
| " .link-button:hover {\n" + | ||
| " background-color: #2980b9;\n" + | ||
| " }\n" + | ||
| "</style>\n" + | ||
| "</head>\n" + | ||
| "<body>\n" + | ||
| "<a href=%s/api/v1/email-cert/cert/%s/%s/%s target=\"_blank\" class=\"link-button\"><div class=\"text-color\">인증하기</div></a>\n" + | ||
| "\n" + | ||
| "</body>\n" + | ||
| "</html>"; | ||
| } |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 근데 상수면 ENUM 쓰는 게 좋을 것 같은데, 전부 class를 사용하신 이유가 있는 건지 궁금해요 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package io.oopy.coding.common.constant; | ||
|
|
||
| import lombok.AccessLevel; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @NoArgsConstructor(access = AccessLevel.PRIVATE) | ||
| public class GlobalConstant { | ||
| public static final String PROJECT_NAME = "팔만코딩경"; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package io.oopy.coding.common.email.dto; | ||
|
|
||
| import lombok.*; | ||
|
|
||
| @Getter | ||
| @AllArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class EmailSend { | ||
|
|
||
| private String targetEmail; | ||
| @Setter | ||
| private String title; | ||
| @Setter | ||
| private String content; | ||
|
|
||
| public static EmailSend of(String targetEmail, String title, String content) { | ||
| return new EmailSend(targetEmail, | ||
| title, | ||
| content | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package io.oopy.coding.common.email.service; | ||
|
|
||
| import io.oopy.coding.common.email.dto.EmailSend; | ||
| import jakarta.mail.Message; | ||
| import jakarta.mail.MessagingException; | ||
| import jakarta.mail.internet.MimeMessage; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.mail.javamail.JavaMailSender; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| /** | ||
| * email db를 만들 가능성이 있으므로, service | ||
| */ | ||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class EmailService { | ||
|
|
||
| private final JavaMailSender javaMailSender; | ||
| private static final String MAIL_CHARACTER_SET = "utf-8"; | ||
| private static final String MAIL_SUB_TYPE = "html"; | ||
|
|
||
| public void sendMail(EmailSend emailSend) throws MessagingException { | ||
| MimeMessage message = javaMailSender.createMimeMessage(); | ||
| message.addRecipients(Message.RecipientType.TO, emailSend.getTargetEmail()); | ||
| message.setSubject(emailSend.getTitle()); | ||
| message.setText(emailSend.getContent(), MAIL_CHARACTER_SET, MAIL_SUB_TYPE); | ||
| javaMailSender.send(message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package io.oopy.coding.common.redis.organizationcert; | ||
|
|
||
| import com.fasterxml.jackson.databind.annotation.JsonDeserialize; | ||
| import com.fasterxml.jackson.databind.annotation.JsonSerialize; | ||
| import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; | ||
| import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; | ||
| import lombok.*; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.time.temporal.ChronoUnit; | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor(access = AccessLevel.PRIVATE) | ||
| @EqualsAndHashCode | ||
| public class OrganizationCertification { | ||
|
|
||
| private String id; | ||
| private String certToken; | ||
| private String userEmail; | ||
| private String organizationCode; | ||
| private String organizationName; | ||
| @JsonSerialize(using = LocalDateTimeSerializer.class) | ||
| @JsonDeserialize(using = LocalDateTimeDeserializer.class) | ||
| private LocalDateTime expiredAt; | ||
| private Boolean expired; | ||
|
|
||
| @Builder | ||
| private OrganizationCertification(String certToken, String id, String userEmail, | ||
| String organizationCode, String organizationName, long ttl) { | ||
| this.certToken = certToken; | ||
| this.id = id; | ||
| this.userEmail = userEmail; | ||
| this.organizationCode = organizationCode; | ||
| this.organizationName = organizationName; | ||
| this.expiredAt = LocalDateTime.now().plus(ttl, ChronoUnit.SECONDS); | ||
| } | ||
|
|
||
| public static OrganizationCertification of(String certToken, String id, String userEmail, | ||
| String organizationCode, String organizationName, long ttl) { | ||
| return new OrganizationCertification(certToken, id, userEmail, | ||
| organizationCode, organizationName, ttl); | ||
| } | ||
|
|
||
| public boolean isExpired() { | ||
| return this.expiredAt.isAfter(LocalDateTime.now()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package io.oopy.coding.common.redis.organizationcert; | ||
|
|
||
| import org.springframework.data.repository.CrudRepository; | ||
|
|
||
| public interface OrganizationCertificationRepository extends CrudRepository<OrganizationCertification, String> { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| package io.oopy.coding.common.redis.organizationcert; | ||
|
|
||
| import io.oopy.coding.common.redis.organizationcert.dto.OrganizationCertificationDto; | ||
| import io.oopy.coding.common.redis.organizationcert.dto.OrganizationCertKey; | ||
| import io.oopy.coding.common.redis.organizationcert.dto.OrganizationKey; | ||
| import io.oopy.coding.organization.service.OrganizationService; | ||
| import io.oopy.coding.organization.service.dto.OrganizationDto; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.data.redis.core.ListOperations; | ||
| import org.springframework.data.redis.core.RedisTemplate; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
| import org.springframework.util.ObjectUtils; | ||
|
|
||
| import java.time.ZoneId; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Optional; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| /** | ||
| * 조직 인증 관련 서비스. Redis를 사용 | ||
| */ | ||
| @Service | ||
| @Slf4j | ||
| public class OrganizationCertificationService { | ||
|
|
||
| private final ListOperations<String, OrganizationCertification> listOperations; | ||
| private final RedisTemplate<String, OrganizationCertification> redisTemplate; | ||
| private final OrganizationService organizationService; | ||
| private static final long EXPIRED_TIME = 600; | ||
|
|
||
| public OrganizationCertificationService(RedisTemplate<String, OrganizationCertification> redisTemplate, | ||
| OrganizationService organizationService) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이건 궁금한 건데, |
||
| this.listOperations = redisTemplate.opsForList(); | ||
| this.redisTemplate = redisTemplate; | ||
| this.organizationService = organizationService; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OrganizationService가 Redis Service를 참조하는 게 아니라, |
||
| } | ||
|
|
||
| /** | ||
| * 인증 진행중인 조직 정보 리스트 조회 | ||
| * @param organizationCertKey | ||
| * @return | ||
| */ | ||
| @Transactional(readOnly = true) | ||
| public List<OrganizationCertification> getOrganizationCertifications(OrganizationKey organizationCertKey) { | ||
| List<OrganizationCertification> organizationCertifications = listOperations.range(organizationCertKey.getKey(), 0, -1); | ||
| if (ObjectUtils.isEmpty(organizationCertifications)) | ||
| return new ArrayList<>(); | ||
| List<OrganizationCertification> filteredOrganizationCertifications = organizationCertifications.stream() | ||
| .filter(v -> v.isExpired()) | ||
| .collect(Collectors.toList()); | ||
| return ObjectUtils.isEmpty(filteredOrganizationCertifications) ? | ||
| new ArrayList<>() : | ||
| filteredOrganizationCertifications; | ||
| } | ||
|
|
||
| /** | ||
| * 조직 인증 등록. 만료시간 : 600초 | ||
| * @param register | ||
| */ | ||
| @Transactional | ||
| public void register(OrganizationCertificationDto.Register register) { | ||
| if(organizationService.isUserAlreadyRegistered( | ||
| OrganizationDto.Cert.of(register.getOrganizationCode(), | ||
| Long.parseLong(register.userId()), | ||
| register.userEmail() | ||
| ))) { | ||
| throw new RuntimeException("이미 인증된 조직입니다."); | ||
| } | ||
| Optional<OrganizationCertification> checkOrganizationCertification = getOrganizationCertification(register); | ||
| if (checkOrganizationCertification.isPresent()) { | ||
| throw new RuntimeException("인증 진행중입니다."); | ||
| } | ||
| OrganizationCertification organizationCertification = OrganizationCertification.of(register.token(), | ||
| register.userId(), | ||
| register.userEmail(), | ||
| register.code(), | ||
| register.organizationName(), | ||
| EXPIRED_TIME | ||
| ); | ||
| listOperations.rightPush(register.getKey(), organizationCertification); | ||
| ZoneId zoneId = ZoneId.systemDefault(); | ||
| redisTemplate.expireAt(register.getKey(), | ||
| organizationCertification.getExpiredAt().atZone(zoneId).toInstant()); | ||
| } | ||
|
|
||
| /** | ||
| * 조직 인증 삭제 | ||
| * @param organizationCertKey | ||
| */ | ||
| @Transactional | ||
| public void delete(OrganizationCertKey organizationCertKey) { | ||
| Optional<OrganizationCertification> organizationCertification = getOrganizationCertification(organizationCertKey); | ||
| if (organizationCertification.isEmpty()) { | ||
| throw new RuntimeException("인증 내역 없음."); | ||
| } | ||
| listOperations.remove(organizationCertKey.getKey(), 0, organizationCertification.get()); | ||
| } | ||
|
|
||
| /** | ||
| * 진행 중인 조직 정보 리스트 중에, 특정 조직 하나를 뽑아낸다. | ||
| * @param cert | ||
| * @return | ||
| */ | ||
| public Optional<OrganizationCertification> getOrganizationCertification(OrganizationCertKey cert) { | ||
| List<OrganizationCertification> organizationCertificationList = getOrganizationCertifications(cert); | ||
| if (ObjectUtils.isEmpty(organizationCertificationList)) { | ||
| return Optional.empty(); | ||
| } | ||
| Optional<OrganizationCertification> organizationCertification = organizationCertificationList.stream() | ||
| .filter(v -> v.getOrganizationCode() != null && | ||
| v.getOrganizationCode().equals(cert.getOrganizationCode())) | ||
| .findFirst(); | ||
| return organizationCertification; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package io.oopy.coding.common.redis.organizationcert.dto; | ||
|
|
||
| public interface OrganizationCertKey extends OrganizationKey{ | ||
| String getOrganizationCode(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이게 뭐하는 api인지는 모르겠지만,
/api/vi/feed/title,/api/v1/feed/body보다 QueryParam으로 받는 게 낫지 않을까요?예를 들어,
/api/v1/feed?type=title이라던가. 그러면 api도 하나로 줄일 수 있을 것 같아서요.