Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.solidconnection.admin.service;

import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_ALREADY_EXISTS;
import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_NOT_FOUND;
import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND;

Expand All @@ -9,9 +10,11 @@
import com.example.solidconnection.admin.dto.MentorApplicationSearchCondition;
import com.example.solidconnection.admin.dto.MentorApplicationSearchResponse;
import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.mentor.domain.Mentor;
import com.example.solidconnection.mentor.domain.MentorApplication;
import com.example.solidconnection.mentor.domain.MentorApplicationStatus;
import com.example.solidconnection.mentor.repository.MentorApplicationRepository;
import com.example.solidconnection.mentor.repository.MentorRepository;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
import com.example.solidconnection.university.domain.HostUniversity;
Expand All @@ -31,6 +34,7 @@ public class AdminMentorApplicationService {
private final MentorApplicationRepository mentorApplicationRepository;
private final HostUniversityRepository hostUniversityRepository;
private final SiteUserRepository siteUserRepository;
private final MentorRepository mentorRepository;

@Transactional(readOnly = true)
public Page<MentorApplicationSearchResponse> searchMentorApplications(
Expand All @@ -45,7 +49,26 @@ public void approveMentorApplication(Long mentorApplicationId) {
MentorApplication mentorApplication = mentorApplicationRepository.findById(mentorApplicationId)
.orElseThrow(() -> new CustomException(MENTOR_APPLICATION_NOT_FOUND));

SiteUser siteUser = siteUserRepository.findById(mentorApplication.getSiteUserId())
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));
validateUserCanCreateMentor(siteUser.getId());

mentorApplication.approve();
siteUser.becomeMentor();

Mentor mentor = new Mentor(
siteUser.getId(),
mentorApplication.getUniversityId(),
mentorApplication.getTermId()
);

mentorRepository.save(mentor);
}

private void validateUserCanCreateMentor(long siteUserId) {
if (mentorRepository.existsBySiteUserId(siteUserId)) {
throw new CustomException(MENTOR_ALREADY_EXISTS);
}
}
Comment on lines +68 to 72
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Mentor 관련 마이그레이션 파일 찾기
find . -type f -name "*.sql" -o -name "*Mentor*Migration*.java" | head -20

Repository: solid-connection/solid-connect-server

Length of output: 1469


🏁 Script executed:

# site_user_id 유니크 제약 관련 SQL 패턴 검색
rg -i 'unique.*site_user_id|site_user_id.*unique' --type sql -A 2 -B 2

Repository: solid-connection/solid-connect-server

Length of output: 2555


🏁 Script executed:

# Mentor 엔티티 클래스 조회
find . -type f -name "Mentor.java" | grep -E 'entity|domain|model'

Repository: solid-connection/solid-connect-server

Length of output: 150


🏁 Script executed:

# 전체 Mentor 엔티티 파일 위치 찾기
fd -t f "Mentor.java"

Repository: solid-connection/solid-connect-server

Length of output: 148


🏁 Script executed:

# Mentor 엔티티 파일 확인
cat -n ./src/main/java/com/example/solidconnection/mentor/domain/Mentor.java

Repository: solid-connection/solid-connect-server

Length of output: 3974


🏁 Script executed:

# V19 마이그레이션 파일 (Mentor 테이블 생성) 확인
cat -n ./src/main/resources/db/migration/V19__create_mentor_related_tables.sql

Repository: solid-connection/solid-connect-server

Length of output: 2317


🏁 Script executed:

# V26 마이그레이션 파일 (term column 추가) 확인
cat -n ./src/main/resources/db/migration/V26__add_term_column_to_mentor.sql

Repository: solid-connection/solid-connect-server

Length of output: 155


멘토 중복 생성을 방지하기 위해 데이터베이스 레벨 유니크 제약을 추가하세요.

검증 결과, 현재 코드는 다음과 같은 상황입니다:

  1. 애플리케이션 레벨 검증

    • existsBySiteUserId() 체크로 비즈니스 규칙을 지키고 있습니다.
  2. 데이터베이스 레벨 보호 부재 (권장 개선)

    • mentor 테이블에 site_user_id 컬럼에 대한 UNIQUE 제약이 없습니다.
    • Mentor 엔티티에도 @Table(uniqueConstraints) 정의가 없습니다.
    • TOCTOU(Time-of-Check-to-Time-of-Use) 경쟁 조건이 발생할 수 있습니다.
  3. 권장사항

    • 마이그레이션 파일에서 ALTER TABLE mentor ADD CONSTRAINT uk_mentor_site_user_id UNIQUE (site_user_id); 추가
    • 또는 Mentor 엔티티에 @Table(uniqueConstraints = @UniqueConstraint(columnNames = "siteUserId")) 추가

어드민 전용 기능이라 동시 호출 가능성은 낮지만, 데이터 무결성을 위해 DB 레벨 제약을 추가하면 더욱 안전합니다.

🤖 Prompt for AI Agents
In
`@src/main/java/com/example/solidconnection/admin/service/AdminMentorApplicationService.java`
around lines 68 - 72, Add a DB-level UNIQUE constraint on the
mentor.site_user_id to prevent TOCTOU race conditions in addition to the
existing application check in validateUserCanCreateMentor (which uses
mentorRepository.existsBySiteUserId). Fix this by adding a migration that runs
ALTER TABLE mentor ADD CONSTRAINT uk_mentor_site_user_id UNIQUE (site_user_id)
and/or annotate the Mentor entity with `@Table`(uniqueConstraints =
`@UniqueConstraint`(columnNames = "siteUserId")) (ensure the column name matches
the mapped column, e.g., site_user_id), and keep the existing existsBySiteUserId
check to preserve the friendly CustomException(MENTOR_ALREADY_EXISTS) for
pre-checks.


@Transactional
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.example.solidconnection.mentor.controller;

import com.example.solidconnection.common.resolver.AuthorizedUser;
import com.example.solidconnection.mentor.dto.MentorMyPageCreateRequest;
import com.example.solidconnection.mentor.dto.MentorMyPageResponse;
import com.example.solidconnection.mentor.dto.MentorMyPageUpdateRequest;
import com.example.solidconnection.mentor.service.MentorMyPageService;
Expand All @@ -11,7 +10,6 @@
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand Down Expand Up @@ -42,14 +40,4 @@ public ResponseEntity<Void> updateMentorMyPage(
mentorMyPageService.updateMentorMyPage(siteUserId, mentorMyPageUpdateRequest);
return ResponseEntity.ok().build();
}

@RequireRoleAccess(roles = Role.MENTOR)
@PostMapping
public ResponseEntity<Void> createMentorMyPage(
@AuthorizedUser long siteUserId,
@Valid @RequestBody MentorMyPageCreateRequest request
) {
mentorMyPageService.createMentorMyPage(siteUserId, request);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ public class Mentor extends BaseEntity {
@Column
private boolean hasBadge = false;

@Column(length = 1000, nullable = false)
@Column(length = 1000)
private String introduction;

@Column(length = 1000, nullable = false)
@Column(length = 1000)
private String passTip;

@Column
Expand Down Expand Up @@ -67,6 +67,16 @@ public Mentor(
this.termId = termId;
}

public Mentor(
long siteUserId,
Long universityId,
long termId
) {
this.siteUserId = siteUserId;
this.universityId = universityId;
this.termId = termId;
}

public void increaseMenteeCount() {
this.menteeCount++;
}
Expand Down Expand Up @@ -96,11 +106,4 @@ public void updateChannels(List<Channel> channels) {
}
}
}

public void createChannels(List<Channel> channels) {
for(Channel channel : channels) {
channel.updateMentor(this);
this.channels.add(channel);
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.example.solidconnection.mentor.service;

import static com.example.solidconnection.common.exception.ErrorCode.CHANNEL_REGISTRATION_LIMIT_EXCEEDED;
import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_ALREADY_EXISTS;
import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_NOT_FOUND;
import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_NOT_FOUND;
import static com.example.solidconnection.common.exception.ErrorCode.TERM_NOT_FOUND;
import static com.example.solidconnection.common.exception.ErrorCode.UNIVERSITY_NOT_FOUND;
Expand All @@ -11,10 +9,7 @@
import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.mentor.domain.Channel;
import com.example.solidconnection.mentor.domain.Mentor;
import com.example.solidconnection.mentor.domain.MentorApplication;
import com.example.solidconnection.mentor.domain.MentorApplicationStatus;
import com.example.solidconnection.mentor.dto.ChannelRequest;
import com.example.solidconnection.mentor.dto.MentorMyPageCreateRequest;
import com.example.solidconnection.mentor.dto.MentorMyPageResponse;
import com.example.solidconnection.mentor.dto.MentorMyPageUpdateRequest;
import com.example.solidconnection.mentor.repository.MentorApplicationRepository;
Expand Down Expand Up @@ -65,50 +60,17 @@ public void updateMentorMyPage(long siteUserId, MentorMyPageUpdateRequest reques

mentor.updateIntroduction(request.introduction());
mentor.updatePassTip(request.passTip());
updateChannel(request.channels(), mentor);
}

private void updateChannel(List<ChannelRequest> channelRequests, Mentor mentor) {
List<Channel> newChannels = buildChannels(channelRequests);
List<Channel> newChannels = buildChannels(request.channels());
mentor.updateChannels(newChannels);
}

@Transactional
public void createMentorMyPage(long siteUserId, MentorMyPageCreateRequest request) {
validateUserCanCreateMentor(siteUserId);
validateChannelRegistrationLimit(request.channels());
MentorApplication mentorApplication = mentorApplicationRepository.findBySiteUserIdAndMentorApplicationStatus(siteUserId, MentorApplicationStatus.APPROVED)
.orElseThrow(() -> new CustomException(MENTOR_APPLICATION_NOT_FOUND));

Mentor mentor = new Mentor(
request.introduction(),
request.passTip(),
siteUserId,
mentorApplication.getUniversityId(),
mentorApplication.getTermId()
);

createChannels(request.channels(), mentor);
mentorRepository.save(mentor);
}

private void validateUserCanCreateMentor(long siteUserId) {
if (mentorRepository.existsBySiteUserId(siteUserId)) {
throw new CustomException(MENTOR_ALREADY_EXISTS);
}
}

private void validateChannelRegistrationLimit(List<ChannelRequest> channelRequests) {
if (channelRequests.size() > CHANNEL_REGISTRATION_LIMIT) {
throw new CustomException(CHANNEL_REGISTRATION_LIMIT_EXCEEDED);
}
}

private void createChannels(List<ChannelRequest> channelRequests, Mentor mentor) {
List<Channel> newChannels = buildChannels(channelRequests);
mentor.createChannels(newChannels);
}

private List<Channel> buildChannels(List<ChannelRequest> channelRequests) {
int sequence = CHANNEL_SEQUENCE_START_NUMBER;
List<Channel> newChannels = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,8 @@ public void updatePassword(String newEncodedPassword) {
public void updateUserStatus(UserStatus status) {
this.userStatus = status;
}

public void becomeMentor() {
this.role = Role.MENTOR;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE mentor
MODIFY introduction VARCHAR(1000) NULL;

ALTER TABLE mentor
MODIFY pass_tip VARCHAR(1000) NULL;
Loading
Loading