-
Notifications
You must be signed in to change notification settings - Fork 8
feat: 학교 이메일 인증으로 HomeUniversity 자동 매핑 #752
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
Changes from all commits
a8f5d84
5f74cb1
bc19c7b
4be2aed
6552e66
a6b0c10
cd61823
1aa18f2
906f77e
06e3afa
148fdcf
501791a
c454f6b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package com.example.solidconnection.common.mail; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.mail.SimpleMailMessage; | ||
| import org.springframework.mail.javamail.JavaMailSender; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class MailService { | ||
|
|
||
| private final JavaMailSender javaMailSender; | ||
|
|
||
| public void sendVerificationEmail(String to, String verificationCode) { | ||
| SimpleMailMessage message = new SimpleMailMessage(); | ||
| message.setTo(to); | ||
| message.setSubject("[Solid Connect] 학교 이메일 인증"); | ||
| message.setText("인증 코드: " + verificationCode + "\n\n인증 코드는 5분간 유효합니다."); | ||
| javaMailSender.send(message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.example.solidconnection.siteuser.dto; | ||
|
|
||
| import jakarta.validation.constraints.NotBlank; | ||
|
|
||
| public record SchoolEmailConfirmRequest( | ||
| @NotBlank(message = "인증 코드는 필수입니다") | ||
| String code | ||
| ) { | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.example.solidconnection.siteuser.dto; | ||
|
|
||
| import jakarta.validation.constraints.Email; | ||
| import jakarta.validation.constraints.NotBlank; | ||
|
|
||
| public record SchoolEmailRequest( | ||
| @NotBlank(message = "학교 이메일은 필수입니다") | ||
| @Email(message = "올바른 이메일 형식이 아닙니다") | ||
| String schoolEmail | ||
| ) { | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package com.example.solidconnection.siteuser.dto; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class SchoolVerificationInfo { | ||
|
|
||
| private String schoolEmail; | ||
| private Long homeUniversityId; | ||
| private String code; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| package com.example.solidconnection.siteuser.service; | ||
|
|
||
| import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_ALREADY_VERIFIED; | ||
| import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_CONFIRM_CODE_DIFFERENT; | ||
| import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_CONFIRM_REQUEST_NOT_FOUND; | ||
| import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_DOMAIN_NOT_SUPPORTED; | ||
| import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_VERIFICATION_INFO_CORRUPTED; | ||
| import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_VERIFICATION_INFO_SAVE_FAILED; | ||
| import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; | ||
|
|
||
| import com.example.solidconnection.common.exception.CustomException; | ||
| import com.example.solidconnection.common.mail.MailService; | ||
| import com.example.solidconnection.siteuser.domain.SiteUser; | ||
| import com.example.solidconnection.siteuser.dto.SchoolVerificationInfo; | ||
| import com.example.solidconnection.siteuser.repository.SiteUserRepository; | ||
| import com.example.solidconnection.university.domain.HomeUniversity; | ||
| import com.example.solidconnection.university.repository.HomeUniversityRepository; | ||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import java.util.concurrent.ThreadLocalRandom; | ||
| import java.util.concurrent.TimeUnit; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.data.redis.core.RedisTemplate; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class SchoolEmailService { | ||
|
|
||
| private static final long VERIFICATION_CODE_TTL_SECONDS = 300; | ||
| private static final String KEY_PREFIX = "school-email:"; | ||
|
|
||
| private final SiteUserRepository siteUserRepository; | ||
| private final HomeUniversityRepository homeUniversityRepository; | ||
| private final MailService mailService; | ||
| private final RedisTemplate<String, String> redisTemplate; | ||
| private final ObjectMapper objectMapper; | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public void requestSchoolEmailVerification(long siteUserId, String schoolEmail) { | ||
|
Member
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. 트랜잭션 어노테이션 달아주세요 !
Member
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. DB 조회만 하기에 |
||
| SiteUser siteUser = siteUserRepository.findById(siteUserId) | ||
| .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); | ||
|
|
||
| if (siteUser.getHomeUniversityId() != null) { | ||
| throw new CustomException(SCHOOL_EMAIL_ALREADY_VERIFIED); | ||
| } | ||
|
|
||
| String domain = extractEmailDomain(schoolEmail); | ||
| HomeUniversity homeUniversity = homeUniversityRepository.findByEmailDomain(domain) | ||
| .orElseThrow(() -> new CustomException(SCHOOL_EMAIL_DOMAIN_NOT_SUPPORTED)); | ||
|
|
||
| String code = generateVerificationCode(); | ||
| saveVerificationInfo(siteUserId, new SchoolVerificationInfo(schoolEmail, homeUniversity.getId(), code)); | ||
|
|
||
| try { | ||
| mailService.sendVerificationEmail(schoolEmail, code); | ||
| } catch (Exception e) { | ||
| redisTemplate.delete(KEY_PREFIX + siteUserId); | ||
| throw e; | ||
| } | ||
| } | ||
|
|
||
| @Transactional | ||
| public void confirmSchoolEmail(long siteUserId, String code) { | ||
| SiteUser siteUser = siteUserRepository.findById(siteUserId) | ||
| .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); | ||
|
|
||
| SchoolVerificationInfo info = getVerificationInfo(siteUserId); | ||
|
|
||
| if (!info.getCode().equals(code)) { | ||
| throw new CustomException(SCHOOL_EMAIL_CONFIRM_CODE_DIFFERENT); | ||
| } | ||
|
|
||
| siteUser.verifySchool(info.getHomeUniversityId()); | ||
| redisTemplate.delete(KEY_PREFIX + siteUserId); | ||
| } | ||
|
|
||
| private void saveVerificationInfo(long siteUserId, SchoolVerificationInfo info) { | ||
| try { | ||
| redisTemplate.opsForValue().set( | ||
| KEY_PREFIX + siteUserId, | ||
| objectMapper.writeValueAsString(info), | ||
| VERIFICATION_CODE_TTL_SECONDS, | ||
| TimeUnit.SECONDS | ||
| ); | ||
| } catch (JsonProcessingException e) { | ||
| throw new CustomException(SCHOOL_EMAIL_VERIFICATION_INFO_SAVE_FAILED); | ||
| } | ||
| } | ||
|
|
||
| private SchoolVerificationInfo getVerificationInfo(long siteUserId) { | ||
| String jsonInfo = redisTemplate.opsForValue().get(KEY_PREFIX + siteUserId); | ||
| if (jsonInfo == null) { | ||
| throw new CustomException(SCHOOL_EMAIL_CONFIRM_REQUEST_NOT_FOUND); | ||
| } | ||
| try { | ||
| return objectMapper.readValue(jsonInfo, SchoolVerificationInfo.class); | ||
| } catch (JsonProcessingException e) { | ||
| redisTemplate.delete(KEY_PREFIX + siteUserId); | ||
| throw new CustomException(SCHOOL_EMAIL_VERIFICATION_INFO_CORRUPTED); | ||
| } | ||
| } | ||
|
|
||
| private String extractEmailDomain(String email) { | ||
|
Member
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. DTO에서 이미
Contributor
Author
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. 중복 검증이네요 바로 substring으로 잘라도 무방할것같습니다 수정하겠음돠 |
||
| return email.substring(email.indexOf('@') + 1).toLowerCase(); | ||
| } | ||
|
Comment on lines
+105
to
+107
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. 이메일 형식 검증 강화 필요 현재 개선 방안:
🤖 Prompt for AI Agents |
||
|
|
||
| private String generateVerificationCode() { | ||
| return String.valueOf(ThreadLocalRandom.current().nextInt(100000, 1000000)); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.