diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java index ef188833b..0f5e46c23 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java @@ -10,6 +10,7 @@ import com.example.solidconnection.siteuser.domain.Role; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import java.util.HashMap; import java.util.Map; import java.util.Objects; import lombok.RequiredArgsConstructor; @@ -20,6 +21,7 @@ public class AuthTokenProvider { private static final String ROLE_CLAIM_KEY = "role"; + private static final String HOME_UNIVERSITY_CLAIM_KEY = "home_university"; private final TokenProvider tokenProvider; private final TokenStorage tokenStorage; @@ -29,9 +31,14 @@ public class AuthTokenProvider { public AccessToken generateAccessToken(SiteUser siteUser) { Subject subject = toSubject(siteUser); Role role = siteUser.getRole(); + Map claims = new HashMap<>(Map.of(ROLE_CLAIM_KEY, role.name())); + if (siteUser.getHomeUniversityId() != null) { + claims.put(HOME_UNIVERSITY_CLAIM_KEY, String.valueOf(siteUser.getHomeUniversityId())); + } + String token = tokenProvider.generateToken( subject, - Map.of(ROLE_CLAIM_KEY, role.name()), + claims, tokenProperties.access().expireTime() ); return new AccessToken(token); @@ -71,6 +78,11 @@ public SiteUser parseSiteUser(String token) { .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); } + public Long parseHomeUniversityId(String token) { + String value = tokenProvider.parseClaims(token, HOME_UNIVERSITY_CLAIM_KEY, String.class); + return value != null ? Long.parseLong(value) : null; + } + public Subject toSubject(SiteUser siteUser) { return new Subject(siteUser.getId().toString()); } diff --git a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java index 5f65329e7..78b653da8 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -1,6 +1,7 @@ package com.example.solidconnection.common.exception; import static com.example.solidconnection.application.service.ApplicationSubmissionService.APPLICATION_UPDATE_COUNT_LIMIT; +import static com.example.solidconnection.mentor.service.MentorApplicationService.MENTOR_APPLICATION_COUNT_LIMIT; import static com.example.solidconnection.siteuser.service.MyPageService.MIN_DAYS_BETWEEN_NICKNAME_CHANGES; import lombok.AllArgsConstructor; @@ -133,6 +134,7 @@ public enum ErrorCode { UNAUTHORIZED_MENTORING(HttpStatus.FORBIDDEN.value(), "멘토링 권한이 없습니다."), MENTORING_ALREADY_CONFIRMED(HttpStatus.BAD_REQUEST.value(), "이미 승인 또는 거절된 멘토링입니다."), MENTOR_APPLICATION_ALREADY_EXISTED(HttpStatus.CONFLICT.value(), "멘토 승격 요청이 이미 존재합니다."), + MENTOR_APPLICATION_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST.value(), "멘토 승격 요청은 " + MENTOR_APPLICATION_COUNT_LIMIT + "회까지만 가능합니다."), INVALID_EXCHANGE_STATUS_FOR_MENTOR(HttpStatus.BAD_REQUEST.value(), "멘토 승격 지원 가능한 교환학생 상태가 아닙니다."), UNIVERSITY_ID_REQUIRED_FOR_CATALOG(HttpStatus.BAD_REQUEST.value(), "목록에서 학교를 선택한 경우 학교 정보가 필요합니다."), UNIVERSITY_ID_MUST_BE_NULL_FOR_OTHER(HttpStatus.BAD_REQUEST.value(), "기타 학교를 선택한 경우 학교 정보를 입력할 수 없습니다."), diff --git a/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java b/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java index 762dcc33e..9a6f4c1bc 100644 --- a/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java +++ b/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java @@ -1,6 +1,7 @@ package com.example.solidconnection.mentor.service; import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_ALREADY_EXISTED; +import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_LIMIT_EXCEEDED; import static com.example.solidconnection.common.exception.ErrorCode.TERM_NOT_FOUND; import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; @@ -28,6 +29,8 @@ @Slf4j public class MentorApplicationService { + public static final int MENTOR_APPLICATION_COUNT_LIMIT = 5; + private final MentorApplicationRepository mentorApplicationRepository; private final SiteUserRepository siteUserRepository; private final S3Service s3Service; @@ -40,6 +43,7 @@ public void submitMentorApplication( MultipartFile file ) { ensureNoPendingOrApprovedMentorApplication(siteUserId); + ensureApplicationCountNotExceeded(siteUserId); SiteUser siteUser = siteUserRepository.findById(siteUserId) .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); @@ -66,4 +70,10 @@ private void ensureNoPendingOrApprovedMentorApplication(long siteUserId) { throw new CustomException(MENTOR_APPLICATION_ALREADY_EXISTED); } } + + private void ensureApplicationCountNotExceeded(long siteUserId) { + if (mentorApplicationRepository.countBySiteUserId(siteUserId) >= MENTOR_APPLICATION_COUNT_LIMIT) { + throw new CustomException(MENTOR_APPLICATION_LIMIT_EXCEEDED); + } + } } diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java index 507e8ab62..2a53348ad 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -50,6 +50,10 @@ public class SiteUser extends BaseEntity { @Column(name = "nickname", nullable = false, length = 100) private String nickname; + @Setter + @Column(name = "home_university_id", nullable = true) + private Long homeUniversityId; + @Setter @Column(name = "profile_image_url", length = 500) private String profileImageUrl; @@ -126,6 +130,27 @@ public SiteUser( this.userStatus = userStatus; } + public SiteUser( + String email, + String nickname, + Long homeUniversityId, + String profileImageUrl, + ExchangeStatus exchangeStatus, + Role role, + AuthType authType, + String password, + UserStatus userStatus) { + this.email = email; + this.nickname = nickname; + this.homeUniversityId = homeUniversityId; + this.profileImageUrl = profileImageUrl; + this.exchangeStatus = exchangeStatus; + this.role = role; + this.authType = authType; + this.password = password; + this.userStatus = userStatus; + } + public void updatePassword(String newEncodedPassword) { this.password = newEncodedPassword; } diff --git a/src/main/resources/config/application-variable.yml b/src/main/resources/config/application-variable.yml index c94a8c671..48cf5e31c 100644 --- a/src/main/resources/config/application-variable.yml +++ b/src/main/resources/config/application-variable.yml @@ -73,7 +73,7 @@ cors: allowed-origins: - "https://www.solid-connection.com" - "https://api.solid-connection.com" - - "https://admin.solid-connection.com" + - "https://admins.solid-connection.com" sentry: environment: "production" diff --git a/src/main/resources/db/migration/V48__add_home_university_id_to_site_user.sql b/src/main/resources/db/migration/V48__add_home_university_id_to_site_user.sql new file mode 100644 index 000000000..b26b5a183 --- /dev/null +++ b/src/main/resources/db/migration/V48__add_home_university_id_to_site_user.sql @@ -0,0 +1,6 @@ +ALTER TABLE site_user + ADD COLUMN home_university_id BIGINT; + +ALTER TABLE site_user + ADD CONSTRAINT fk_site_user_home_university + FOREIGN KEY (home_university_id) REFERENCES home_university(id) ON DELETE NO ACTION; \ No newline at end of file diff --git a/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java index 7a6f64428..6b2d36a11 100644 --- a/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java @@ -10,6 +10,7 @@ import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.university.fixture.HomeUniversityFixture; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -33,12 +34,19 @@ class AuthTokenProviderTest { @Autowired private SiteUserFixture siteUserFixture; + @Autowired + private HomeUniversityFixture homeUniversityFixture; + private SiteUser siteUser; + private SiteUser siteUserWithHomeUniversity; + private Long homeUniversityId; private Subject expectedSubject; @BeforeEach void setUp() { + homeUniversityId = homeUniversityFixture.인하대학교().getId(); siteUser = siteUserFixture.사용자(); + siteUserWithHomeUniversity = siteUserFixture.국내_대학_정보_소지_사용자(homeUniversityId); expectedSubject = new Subject(siteUser.getId().toString()); } @@ -70,6 +78,30 @@ void setUp() { assertThat(actualSitUser.getId()).isEqualTo(siteUser.getId()); } + @Nested + class 액세스_토큰_homeUniversityId_클레임 { + + @Test + void homeUniversityId가_있는_사용자는_액세스_토큰_클레임에_포함된다() { + // when + String token = authTokenProvider.generateAccessToken(siteUserWithHomeUniversity).token(); + + // then + Long actual = authTokenProvider.parseHomeUniversityId(token); + assertThat(actual).isEqualTo(homeUniversityId); + } + + @Test + void homeUniversityId가_없는_사용자는_액세스_토큰_클레임에서_생략된다() { + // when + String token = authTokenProvider.generateAccessToken(siteUser).token(); + + // then + Long actual = authTokenProvider.parseHomeUniversityId(token); + assertThat(actual).isNull(); + } + } + @Nested class 리프레시_토큰을_제공한다 { diff --git a/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java b/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java index 9c148291b..31792e6a8 100644 --- a/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java +++ b/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java @@ -1,8 +1,10 @@ package com.example.solidconnection.mentor.service; import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_ALREADY_EXISTED; +import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_LIMIT_EXCEEDED; import static com.example.solidconnection.common.exception.ErrorCode.UNIVERSITY_ID_MUST_BE_NULL_FOR_OTHER; import static com.example.solidconnection.common.exception.ErrorCode.UNIVERSITY_ID_REQUIRED_FOR_CATALOG; +import static com.example.solidconnection.mentor.service.MentorApplicationService.MENTOR_APPLICATION_COUNT_LIMIT; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; import static org.mockito.BDDMockito.given; @@ -163,6 +165,24 @@ void setUp() { .hasMessage(MENTOR_APPLICATION_ALREADY_EXISTED.getMessage()); } + @Test + void 멘토_승격_신청_횟수가_최대_횟수에_도달하면_예외가_발생한다() { + // given + for (int i = 0; i < MENTOR_APPLICATION_COUNT_LIMIT; i++) { + mentorApplicationFixture.거절된_멘토신청(user.getId(), UniversitySelectType.CATALOG, 1L); + } + + UniversitySelectType universitySelectType = UniversitySelectType.CATALOG; + Long universityId = 1L; + MentorApplicationRequest request = createMentorApplicationRequest(universitySelectType, universityId); + MockMultipartFile file = createMentorProofFile(); + + // when & then + assertThatCode(() -> mentorApplicationService.submitMentorApplication(user.getId(), request, file)) + .isInstanceOf(CustomException.class) + .hasMessage(MENTOR_APPLICATION_LIMIT_EXCEEDED.getMessage()); + } + @Test void 이미_REJECTED_상태인_멘토_승격_요청이_존재할_때_멘토_신청이_등록된다() { // given diff --git a/src/test/java/com/example/solidconnection/siteuser/fixture/SiteUserFixture.java b/src/test/java/com/example/solidconnection/siteuser/fixture/SiteUserFixture.java index 05262b6a2..1753d0d5d 100644 --- a/src/test/java/com/example/solidconnection/siteuser/fixture/SiteUserFixture.java +++ b/src/test/java/com/example/solidconnection/siteuser/fixture/SiteUserFixture.java @@ -61,6 +61,19 @@ public class SiteUserFixture { .create(); } + public SiteUser 국내_대학_정보_소지_사용자(Long homeUniversityId) { + return siteUserFixtureBuilder.siteUser() + .email("university@example.com") + .authType(AuthType.EMAIL) + .nickname("국내대학사용자") + .homeUniversityId(homeUniversityId) + .profileImageUrl("profileImageUrl") + .role(Role.MENTEE) + .password("password123") + .userStatus(UserStatus.ACTIVE) + .create(); + } + public SiteUser 멘토(int index, String nickname) { return siteUserFixtureBuilder.siteUser() .email("mentor" + index + "@example.com") diff --git a/src/test/java/com/example/solidconnection/siteuser/fixture/SiteUserFixtureBuilder.java b/src/test/java/com/example/solidconnection/siteuser/fixture/SiteUserFixtureBuilder.java index e4497f24c..02c361c44 100644 --- a/src/test/java/com/example/solidconnection/siteuser/fixture/SiteUserFixtureBuilder.java +++ b/src/test/java/com/example/solidconnection/siteuser/fixture/SiteUserFixtureBuilder.java @@ -20,6 +20,7 @@ public class SiteUserFixtureBuilder { private String email; private AuthType authType; private String nickname; + private Long homeUniversityId; private String profileImageUrl; private Role role; private String password; @@ -44,6 +45,11 @@ public SiteUserFixtureBuilder nickname(String nickname) { return this; } + public SiteUserFixtureBuilder homeUniversityId(Long homeUniversityId) { + this.homeUniversityId = homeUniversityId; + return this; + } + public SiteUserFixtureBuilder profileImageUrl(String profileImageUrl) { this.profileImageUrl = profileImageUrl; return this; @@ -68,6 +74,7 @@ public SiteUser create() { SiteUser siteUser = new SiteUser( email, nickname, + homeUniversityId, profileImageUrl, ExchangeStatus.CONSIDERING, role,