-
Notifications
You must be signed in to change notification settings - Fork 3
[DDING-000] 이메일 전송 현황 조회 구현 #361
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: develop
Are you sure you want to change the base?
Changes from all commits
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,48 @@ | ||
| package ddingdong.ddingdongBE.domain.form.controller.dto.response; | ||
|
|
||
| import ddingdong.ddingdongBE.domain.form.service.dto.query.EmailSendStatusQuery; | ||
| import ddingdong.ddingdongBE.domain.form.service.dto.query.EmailSendStatusQuery.EmailSendStatusInfoQuery; | ||
| import ddingdong.ddingdongBE.domain.formapplication.entity.FormApplicationStatus; | ||
| import ddingdong.ddingdongBE.email.entity.EmailSendStatus; | ||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
|
|
||
| public record EmailSendStatusResponse( | ||
| List<EmailSendStatusInfoResponse> emailSendStatusInfoResponses | ||
|
|
||
| ) { | ||
|
|
||
| public static EmailSendStatusResponse from(EmailSendStatusQuery emailSendStatusQuery) { | ||
| List<EmailSendStatusInfoResponse> responses = emailSendStatusQuery.emailSendStatusInfoQueries() | ||
| .stream() | ||
| .map(EmailSendStatusInfoResponse::from) | ||
| .toList(); | ||
| return new EmailSendStatusResponse(responses); | ||
| } | ||
|
|
||
| record EmailSendStatusInfoResponse( | ||
| @Schema(description = "지원자 이름", example = "고건") | ||
| String name, | ||
| @Schema(description = "학번", example = "60221300") | ||
| String studentNumber, | ||
| @Schema(description = "이메일 전송 일시", example = "2025.12.13 13:00") | ||
| LocalDateTime sendAt, // TODO: 재전송 시 Update 해주기 | ||
| @Schema(description = "이메일 전송 상태", example = "Sending") | ||
| EmailSendStatus emailSendStatus, | ||
| @Schema(description = "지원자 서류/면접 합불 상태", example = "FIRST_PASS") | ||
| FormApplicationStatus formApplicationStatus | ||
| ) { | ||
|
|
||
| public static EmailSendStatusInfoResponse from( | ||
| EmailSendStatusInfoQuery emailSendStatusInfoQuery) { | ||
| return new EmailSendStatusInfoResponse( | ||
| emailSendStatusInfoQuery.name(), | ||
| emailSendStatusInfoQuery.studentNumber(), | ||
| emailSendStatusInfoQuery.sendAt(), | ||
| emailSendStatusInfoQuery.emailSendStatus(), | ||
| emailSendStatusInfoQuery.formApplicationStatus() | ||
| ); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,14 @@ | ||
| package ddingdong.ddingdongBE.domain.form.repository; | ||
|
|
||
| import ddingdong.ddingdongBE.domain.form.entity.FormEmailSendHistory; | ||
| import ddingdong.ddingdongBE.domain.formapplication.entity.FormApplication; | ||
| import ddingdong.ddingdongBE.domain.formapplication.entity.FormApplicationStatus; | ||
| import java.util.List; | ||
| import java.util.Optional; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface FormEmailSendHistoryRepository extends JpaRepository<FormEmailSendHistory, Long> { | ||
|
|
||
| Optional<FormEmailSendHistory> findByFormIdAndFormApplicationStatus(Long formId, FormApplicationStatus formApplicationStatus); | ||
| List<FormEmailSendHistory> getAllByFormId(Long formId); | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ | |
| import ddingdong.ddingdongBE.domain.form.service.dto.command.UpdateFormCommand.UpdateFormFieldCommand; | ||
| import ddingdong.ddingdongBE.domain.form.service.dto.command.UpdateFormEndDateCommand; | ||
| import ddingdong.ddingdongBE.domain.form.service.dto.query.EmailSendCountQuery; | ||
| import ddingdong.ddingdongBE.domain.form.service.dto.query.EmailSendStatusQuery; | ||
| import ddingdong.ddingdongBE.domain.form.service.dto.query.FormListQuery; | ||
| import ddingdong.ddingdongBE.domain.form.service.dto.query.FormQuery; | ||
| import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery; | ||
|
|
@@ -91,13 +92,15 @@ public void createForm(CreateFormCommand createFormCommand) { | |
| }) | ||
| public void updateForm(UpdateFormCommand command) { | ||
| Club club = clubService.getByUserId(command.user().getId()); | ||
| validateDuplicationDateExcludingSelf(club, command.startDate(), command.endDate(), command.formId()); | ||
| validateDuplicationDateExcludingSelf(club, command.startDate(), command.endDate(), | ||
| command.formId()); | ||
|
|
||
| Form originform = formService.getById(command.formId()); | ||
| Form updateForm = command.toEntity(); | ||
| originform.update(updateForm); | ||
|
|
||
| List<FormField> updatedFormFields = toUpdateFormFields(originform, command.formFieldCommands()); | ||
| List<FormField> updatedFormFields = toUpdateFormFields(originform, | ||
| command.formFieldCommands()); | ||
| originform.updateFormFields(updatedFormFields); | ||
| } | ||
|
|
||
|
|
@@ -164,7 +167,8 @@ public void registerApplicantAsMember(Long userId, Long formId) { | |
| List<ClubMember> originClubMembers = club.getClubMembers(); | ||
| clubMemberService.deleteAll(originClubMembers); | ||
|
|
||
| List<FormApplication> finalPassedFormApplications = formApplicationService.getAllFinalPassedByFormId(formId); | ||
| List<FormApplication> finalPassedFormApplications = formApplicationService.getAllFinalPassedByFormId( | ||
| formId); | ||
| List<ClubMember> finalPassedClubMembers = finalPassedFormApplications.stream() | ||
| .map(ClubMember::createFromFormApplication) | ||
| .toList(); | ||
|
|
@@ -180,10 +184,12 @@ public SingleFieldStatisticsQuery getTextFieldStatistics(Long fieldId) { | |
| } | ||
| String type = formField.getFieldType().name(); | ||
| if (formField.isFile()) { | ||
| List<SingleStatisticsQuery> textStatisticsQueries = formStatisticService.createFileStatistics(formField); | ||
| List<SingleStatisticsQuery> textStatisticsQueries = formStatisticService.createFileStatistics( | ||
| formField); | ||
| return new SingleFieldStatisticsQuery(type, textStatisticsQueries); | ||
| } | ||
| List<SingleStatisticsQuery> textStatisticsQueries = formStatisticService.createTextStatistics(formField); | ||
| List<SingleStatisticsQuery> textStatisticsQueries = formStatisticService.createTextStatistics( | ||
| formField); | ||
| return new SingleFieldStatisticsQuery(type, textStatisticsQueries); | ||
| } | ||
|
|
||
|
|
@@ -197,18 +203,21 @@ public void sendApplicationResultEmail(SendApplicationResultEmailCommand command | |
| ); | ||
| Form form = formService.getById(command.formId()); | ||
| EmailContent emailContent = EmailContent.of(command.title(), command.message(), club); | ||
| FormEmailSendHistory formEmailSendHistory = formEmailSendHistoryService.create(form, command.target(), | ||
| FormEmailSendHistory formEmailSendHistory = formEmailSendHistoryService.create(form, | ||
| command.target(), | ||
| command.message()); | ||
|
|
||
| List<FormResultSendingEmailInfo> formResultSendingEmailInfos = formApplications.stream() | ||
| .map(application -> { | ||
| EmailSendHistory emailSendHistory = emailSendHistoryService.save( | ||
| EmailSendHistory.createPending(application, formEmailSendHistory)); | ||
| return new FormResultSendingEmailInfo(emailSendHistory.getId(), application.getEmail(), | ||
| return new FormResultSendingEmailInfo(emailSendHistory.getId(), | ||
| application.getEmail(), | ||
| application.getName(), emailContent); | ||
| }) | ||
| .toList(); | ||
| applicationEventPublisher.publishEvent(new SendFormResultEvent(formResultSendingEmailInfos)); | ||
| applicationEventPublisher.publishEvent( | ||
| new SendFormResultEvent(formResultSendingEmailInfos)); | ||
| } | ||
|
|
||
| @Transactional | ||
|
|
@@ -217,13 +226,16 @@ public void updateFormEndDate(UpdateFormEndDateCommand command) { | |
| Club club = clubService.getByUserId(command.user().getId()); | ||
| Form form = formService.getById(command.formId()); | ||
| validateEndDate(form.getStartDate(), command.endDate()); | ||
| validateDuplicationDateExcludingSelf(club, form.getStartDate(), command.endDate(), command.formId()); | ||
| validateDuplicationDateExcludingSelf(club, form.getStartDate(), command.endDate(), | ||
| command.formId()); | ||
| form.updateEndDate(command.endDate()); | ||
| } | ||
|
|
||
| @Override | ||
| public EmailSendCountQuery getEmailSendCountByFormEmailSendHistoryId(Long formEmailSendHistoryId) { | ||
| FormEmailSendHistory formEmailSendHistory = formEmailSendHistoryService.getById(formEmailSendHistoryId); | ||
| public EmailSendCountQuery getEmailSendCountByFormEmailSendHistoryId( | ||
| Long formEmailSendHistoryId) { | ||
| FormEmailSendHistory formEmailSendHistory = formEmailSendHistoryService.getById( | ||
| formEmailSendHistoryId); | ||
| EmailSendHistories emailSendHistories = emailSendHistoryService.getAllByFormEmailSendHistoryId( | ||
| formEmailSendHistory.getId()); | ||
|
|
||
|
|
@@ -234,6 +246,20 @@ public EmailSendCountQuery getEmailSendCountByFormEmailSendHistoryId(Long formEm | |
| ); | ||
| } | ||
|
|
||
| @Override | ||
| public EmailSendStatusQuery getEmailSendStatusByFormId(Long formId) { | ||
| List<FormEmailSendHistory> formEmailSendHistories = formEmailSendHistoryService.getAllByFormId( | ||
| formId); | ||
| List<Long> formEmailSendHistoryIds = formEmailSendHistories.stream() | ||
| .map(FormEmailSendHistory::getId) | ||
| .toList(); | ||
| EmailSendHistories emailSendHistories = emailSendHistoryService.getAllByFormEmailSendHistoryIds( | ||
| formEmailSendHistoryIds); | ||
| EmailSendHistories latestEmailSendHistories = emailSendHistories.getLatestByFormApplication(); | ||
|
|
||
| return EmailSendStatusQuery.from(latestEmailSendHistories.getAll()); | ||
| } | ||
|
Comment on lines
+249
to
+261
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. Form 소유권 검증 누락 확인 필요 다른 메서드들( 🔒 소유권 검증 추가 제안 `@Override`
-public EmailSendStatusQuery getEmailSendStatusByFormId(Long formId) {
+public EmailSendStatusQuery getEmailSendStatusByFormId(Long formId, User user) {
+ Club club = clubService.getByUserId(user.getId());
+ Form form = formService.getById(formId);
+ validateEqualsClub(club, form);
+
List<FormEmailSendHistory> formEmailSendHistories = formEmailSendHistoryService.getAllByFormId(
formId);참고: 이 변경을 적용할 경우, 🤖 Prompt for AI Agents |
||
|
|
||
| private void validateEqualsClub(Club club, Form form) { | ||
| if (form.isNotEqualClubId(club.getId())) { | ||
| throw new NonHaveFormAuthority(); | ||
|
|
@@ -259,7 +285,8 @@ private void validateDuplicationDateExcludingSelf( | |
| LocalDate endDate, | ||
| Long formId | ||
| ) { | ||
| List<Form> overlappingForms = formService.findOverlappingForms(club.getId(), startDate, endDate) | ||
| List<Form> overlappingForms = formService.findOverlappingForms(club.getId(), startDate, | ||
| endDate) | ||
| .stream() | ||
| .filter(form -> !form.isEqualsById(formId)) | ||
| .toList(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package ddingdong.ddingdongBE.domain.form.service.dto.query; | ||
|
|
||
| import ddingdong.ddingdongBE.domain.formapplication.entity.FormApplicationStatus; | ||
| import ddingdong.ddingdongBE.email.entity.EmailSendHistory; | ||
| import ddingdong.ddingdongBE.email.entity.EmailSendStatus; | ||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
|
|
||
|
|
||
| public record EmailSendStatusQuery( | ||
| List<EmailSendStatusInfoQuery> emailSendStatusInfoQueries | ||
| ) { | ||
|
|
||
| public static EmailSendStatusQuery from(List<EmailSendHistory> emailSendHistories) { | ||
| List<EmailSendStatusInfoQuery> infos = emailSendHistories.stream() | ||
| .map(EmailSendStatusInfoQuery::from) | ||
| .toList(); | ||
|
|
||
| return new EmailSendStatusQuery(infos); | ||
| } | ||
|
|
||
| public record EmailSendStatusInfoQuery( | ||
| String name, | ||
| String studentNumber, | ||
| LocalDateTime sendAt, | ||
| EmailSendStatus emailSendStatus, | ||
| FormApplicationStatus formApplicationStatus | ||
| ) { | ||
|
|
||
| public static EmailSendStatusInfoQuery from(EmailSendHistory emailSendHistory) { | ||
| return new EmailSendStatusInfoQuery( | ||
| emailSendHistory.getFormApplication().getName(), | ||
| emailSendHistory.getFormApplication().getStudentNumber(), | ||
| emailSendHistory.getSentAt(), | ||
| emailSendHistory.getStatus(), | ||
| emailSendHistory.getFormEmailSendHistory().getFormApplicationStatus() | ||
| ); | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
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.
🧩 Analysis chain
🏁 Script executed:
Repository: COW-dev/ddingdong-be
Length of output: 137
🏁 Script executed:
Repository: COW-dev/ddingdong-be
Length of output: 4047
🏁 Script executed:
Repository: COW-dev/ddingdong-be
Length of output: 10990
🏁 Script executed:
Repository: COW-dev/ddingdong-be
Length of output: 3363
🏁 Script executed:
Repository: COW-dev/ddingdong-be
Length of output: 912
🏁 Script executed:
Repository: COW-dev/ddingdong-be
Length of output: 325
🏁 Script executed:
Repository: COW-dev/ddingdong-be
Length of output: 7661
🏁 Script executed:
Repository: COW-dev/ddingdong-be
Length of output: 46
🏁 Script executed:
Repository: COW-dev/ddingdong-be
Length of output: 1928
🏁 Script executed:
Repository: COW-dev/ddingdong-be
Length of output: 6785
🏁 Script executed:
Repository: COW-dev/ddingdong-be
Length of output: 1209
🏁 Script executed:
Repository: COW-dev/ddingdong-be
Length of output: 3292
폼 소유자 검증 누락으로 인한 IDOR 취약점 존재
경로가
/my/forms/{formId}인데 메서드 시그니처에 인증 사용자 정보가 없어 소유권 검증이 불가능합니다. 서비스 레이어에서도 ownership check가 없어서 다른 사용자가 formId만으로 임의의 폼의 이메일 전송 현황을 조회할 수 있습니다.같은 패턴의
getFormStatistics()(라인 95-99)는@AuthenticationPrincipal PrincipalDetails를 받아 userId를 기반으로validateEqualsClub()을 통해 소유권을 검증합니다. 이 엔드포인트도 동일한 방식으로 수정해야 합니다.✅ 수정 방안
`@GetMapping`("/my/forms/{formId}/emails/status") EmailSendStatusResponse getEmailSendStatus( - `@PathVariable`("formId") Long formId + `@PathVariable`("formId") Long formId, + `@AuthenticationPrincipal` PrincipalDetails principalDetails );그 후 서비스 레이어에서:
📝 Committable suggestion
🤖 Prompt for AI Agents