Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @Gyuhyeok99 @nayonsoso @wibaek
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: CI with Gradle

on:
pull_request:
branches: [ "develop", "release", "master" ]

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
checks: write

steps:
- name: Checkout the code
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Make Gradle wrapper executable
run: chmod +x ./gradlew

- name: Build with Gradle Wrapper
run: ./gradlew build

- name: Publish Test Report
uses: mikepenz/action-junit-report@v5
if: success() || failure()
with:
report_paths: '**/build/test-results/test/TEST-*.xml'
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.example.solidconnection.application.service.ApplicationQueryService;
import com.example.solidconnection.application.service.ApplicationSubmissionService;
import com.example.solidconnection.custom.resolver.AuthorizedUser;
import com.example.solidconnection.custom.security.annotation.RequireAdminAccess;
import com.example.solidconnection.siteuser.domain.SiteUser;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
Expand All @@ -32,12 +33,13 @@ public ResponseEntity<ApplicationSubmissionResponse> apply(
@AuthorizedUser SiteUser siteUser,
@Valid @RequestBody ApplyRequest applyRequest
) {
boolean result = applicationSubmissionService.apply(siteUser, applyRequest);
ApplicationSubmissionResponse applicationSubmissionResponse = applicationSubmissionService.apply(siteUser, applyRequest);
return ResponseEntity
.status(HttpStatus.OK)
.body(new ApplicationSubmissionResponse(result));
.body(applicationSubmissionResponse);
}

@RequireAdminAccess
@GetMapping
public ResponseEntity<ApplicationsResponse> getApplicants(
@AuthorizedUser SiteUser siteUser,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public class Application {
@Column(length = 100)
private String nicknameForApply;

@Column(columnDefinition = "int not null default 0")
@Column(columnDefinition = "int not null default 1")
private Integer updateCount;

@Column(length = 50, nullable = false)
Expand Down Expand Up @@ -76,7 +76,7 @@ public Application(
this.gpa = gpa;
this.languageTest = languageTest;
this.term = term;
this.updateCount = 0;
this.updateCount = 1;
this.verifyStatus = PENDING;
}

Expand Down Expand Up @@ -115,7 +115,7 @@ public Application(
this.gpa = gpa;
this.languageTest = languageTest;
this.term = term;
this.updateCount = 0;
this.updateCount = 1;
this.firstChoiceUniversity = firstChoiceUniversity;
this.secondChoiceUniversity = secondChoiceUniversity;
this.thirdChoiceUniversity = thirdChoiceUniversity;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package com.example.solidconnection.application.dto;

import com.example.solidconnection.application.domain.Application;

public record ApplicationSubmissionResponse(
boolean isSuccess) {
int applyCount
) {
public static ApplicationSubmissionResponse from(Application application) {
return new ApplicationSubmissionResponse(application.getUpdateCount());
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.solidconnection.application.service;

import com.example.solidconnection.application.domain.Application;
import com.example.solidconnection.application.dto.ApplicationSubmissionResponse;
import com.example.solidconnection.application.dto.ApplyRequest;
import com.example.solidconnection.application.dto.UniversityChoiceRequest;
import com.example.solidconnection.application.repository.ApplicationRepository;
Expand Down Expand Up @@ -49,15 +50,10 @@ public class ApplicationSubmissionService {
key = {"applications:all"},
cacheManager = "customCacheManager"
)
public boolean apply(SiteUser siteUser, ApplyRequest applyRequest) {
public ApplicationSubmissionResponse apply(SiteUser siteUser, ApplyRequest applyRequest) {
UniversityChoiceRequest universityChoiceRequest = applyRequest.universityChoiceRequest();

Long gpaScoreId = applyRequest.gpaScoreId();
Long languageTestScoreId = applyRequest.languageTestScoreId();
GpaScore gpaScore = getValidGpaScore(siteUser, gpaScoreId);
LanguageTestScore languageTestScore = getValidLanguageTestScore(siteUser, languageTestScoreId);

Optional<Application> application = applicationRepository.findBySiteUserAndTerm(siteUser, term);
GpaScore gpaScore = getValidGpaScore(siteUser, applyRequest.gpaScoreId());
LanguageTestScore languageTestScore = getValidLanguageTestScore(siteUser, applyRequest.languageTestScoreId());

UniversityInfoForApply firstChoiceUniversity = universityInfoForApplyRepository
.getUniversityInfoForApplyByIdAndTerm(universityChoiceRequest.firstChoiceUniversityId(), term);
Expand All @@ -68,22 +64,19 @@ public boolean apply(SiteUser siteUser, ApplyRequest applyRequest) {
.map(id -> universityInfoForApplyRepository.getUniversityInfoForApplyByIdAndTerm(id, term))
.orElse(null);

if (application.isEmpty()) {
Application newApplication = new Application(siteUser, gpaScore.getGpa(), languageTestScore.getLanguageTest(),
term, firstChoiceUniversity, secondChoiceUniversity, thirdChoiceUniversity, getRandomNickname());
newApplication.setVerifyStatus(VerifyStatus.APPROVED);
applicationRepository.save(newApplication);
} else {
Application before = application.get();
validateUpdateLimitNotExceed(before);
before.setIsDeleteTrue(); // 기존 이력 soft delete 수행한다.

Application newApplication = new Application(siteUser, gpaScore.getGpa(), languageTestScore.getLanguageTest(),
term, before.getUpdateCount() + 1, firstChoiceUniversity, secondChoiceUniversity, thirdChoiceUniversity, getRandomNickname());
newApplication.setVerifyStatus(VerifyStatus.APPROVED);
applicationRepository.save(newApplication);
}
return true;
Optional<Application> existingApplication = applicationRepository.findBySiteUserAndTerm(siteUser, term);
int updateCount = existingApplication
.map(application -> {
validateUpdateLimitNotExceed(application);
application.setIsDeleteTrue();
return application.getUpdateCount() + 1;
})
.orElse(1);
Application newApplication = new Application(siteUser, gpaScore.getGpa(), languageTestScore.getLanguageTest(),
term, updateCount, firstChoiceUniversity, secondChoiceUniversity, thirdChoiceUniversity, getRandomNickname());
newApplication.setVerifyStatus(VerifyStatus.APPROVED);
applicationRepository.save(newApplication);
return ApplicationSubmissionResponse.from(newApplication);
}

private GpaScore getValidGpaScore(SiteUser siteUser, Long gpaScoreId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,17 @@
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.stereotype.Component;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Date;
import java.util.stream.Collectors;

import static com.example.solidconnection.custom.exception.ErrorCode.FAILED_TO_READ_APPLE_PRIVATE_KEY;

/*
* 애플 OAuth 에 필요하 클라이언트 시크릿은 매번 동적으로 생성해야 한다.
* 애플 OAuth 에 필요한 클라이언트 시크릿은 매번 동적으로 생성해야 한다.
* 클라이언트 시크릿은 애플 개발자 계정에서 발급받은 개인키(*.p8)를 사용하여 JWT 를 생성한다.
* https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret
* */
Expand All @@ -32,14 +29,13 @@ public class AppleOAuthClientSecretProvider {

private static final String KEY_ID_HEADER = "kid";
private static final long TOKEN_DURATION = 1000 * 60 * 10; // 10min
private static final String SECRET_KEY_PATH = "secret/AppleOAuthKey.p8";

private final AppleOAuthClientProperties appleOAuthClientProperties;
private PrivateKey privateKey;

@PostConstruct
private void initPrivateKey() {
privateKey = readPrivateKey();
privateKey = loadPrivateKey();
}

public String generateClientSecret() {
Expand All @@ -57,16 +53,14 @@ public String generateClientSecret() {
.compact();
}

private PrivateKey readPrivateKey() {
try (InputStream is = getClass().getClassLoader().getResourceAsStream(SECRET_KEY_PATH);
BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {

String secretKey = reader.lines().collect(Collectors.joining("\n"));
private PrivateKey loadPrivateKey() {
try {
String secretKey = appleOAuthClientProperties.secretKey();
byte[] encoded = Base64.decodeBase64(secretKey);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
KeyFactory keyFactory = KeyFactory.getInstance("EC");
return keyFactory.generatePrivate(keySpec);
} catch (Exception e) {
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new CustomException(FAILED_TO_READ_APPLE_PRIVATE_KEY);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public record AppleOAuthClientProperties(
String publicKeyUrl,
String clientId,
String teamId,
String keyId
String keyId,
String secretKey
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import org.springframework.http.HttpStatus;

import static com.example.solidconnection.application.service.ApplicationSubmissionService.APPLICATION_UPDATE_COUNT_LIMIT;
import static com.example.solidconnection.siteuser.service.SiteUserService.MIN_DAYS_BETWEEN_NICKNAME_CHANGES;
import static com.example.solidconnection.siteuser.service.MyPageService.MIN_DAYS_BETWEEN_NICKNAME_CHANGES;

@Getter
@AllArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.solidconnection.custom.security.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireAdminAccess {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.example.solidconnection.custom.security.aspect;

import com.example.solidconnection.custom.exception.CustomException;
import com.example.solidconnection.custom.security.annotation.RequireAdminAccess;
import com.example.solidconnection.siteuser.domain.SiteUser;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_DENIED;
import static com.example.solidconnection.type.Role.ADMIN;

@Aspect
@Component
@RequiredArgsConstructor
public class AdminAuthorizationAspect {

@Around("@annotation(requireAdminAccess)")
public Object checkAdminAccess(ProceedingJoinPoint joinPoint,
RequireAdminAccess requireAdminAccess) throws Throwable {
SiteUser siteUser = null;
for (Object arg : joinPoint.getArgs()) {
if (arg instanceof SiteUser) {
siteUser = (SiteUser) arg;
break;
}
}
if (siteUser == null || !ADMIN.equals(siteUser.getRole())) {
throw new CustomException(ACCESS_DENIED);
}
return joinPoint.proceed();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
import org.hibernate.annotations.DynamicUpdate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.ZoneId;
import java.time.ZonedDateTime;

import static java.time.ZoneOffset.UTC;
import static java.time.temporal.ChronoUnit.MICROS;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
Expand All @@ -24,12 +26,12 @@ public abstract class BaseEntity {

@PrePersist
public void onPrePersist() {
this.createdAt = ZonedDateTime.now(ZoneId.of("UTC"));
this.createdAt = ZonedDateTime.now(UTC).truncatedTo(MICROS); // 나노초 6자리 까지만 저장
this.updatedAt = this.createdAt;
}

@PreUpdate
public void onPreUpdate() {
this.updatedAt = ZonedDateTime.now(ZoneId.of("UTC"));
this.updatedAt = ZonedDateTime.now(UTC).truncatedTo(MICROS);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import com.example.solidconnection.custom.resolver.AuthorizedUser;
import com.example.solidconnection.score.dto.GpaScoreRequest;
import com.example.solidconnection.score.dto.GpaScoreStatusResponse;
import com.example.solidconnection.score.dto.GpaScoreStatusesResponse;
import com.example.solidconnection.score.dto.LanguageTestScoreRequest;
import com.example.solidconnection.score.dto.LanguageTestScoreStatusResponse;
import com.example.solidconnection.score.dto.LanguageTestScoreStatusesResponse;
import com.example.solidconnection.score.service.ScoreService;
import com.example.solidconnection.siteuser.domain.SiteUser;
import jakarta.validation.Valid;
Expand Down Expand Up @@ -49,19 +49,19 @@ public ResponseEntity<Long> submitLanguageTestScore(

// 학점 상태를 확인하는 api
@GetMapping("/gpas")
public ResponseEntity<GpaScoreStatusResponse> getGpaScoreStatus(
public ResponseEntity<GpaScoreStatusesResponse> getGpaScoreStatus(
@AuthorizedUser SiteUser siteUser
) {
GpaScoreStatusResponse gpaScoreStatus = scoreService.getGpaScoreStatus(siteUser);
GpaScoreStatusesResponse gpaScoreStatus = scoreService.getGpaScoreStatus(siteUser);
return ResponseEntity.ok(gpaScoreStatus);
}

// 어학 성적 상태를 확인하는 api
@GetMapping("/language-tests")
public ResponseEntity<LanguageTestScoreStatusResponse> getLanguageTestScoreStatus(
public ResponseEntity<LanguageTestScoreStatusesResponse> getLanguageTestScoreStatus(
@AuthorizedUser SiteUser siteUser
) {
LanguageTestScoreStatusResponse languageTestScoreStatus = scoreService.getLanguageTestScoreStatus(siteUser);
LanguageTestScoreStatusesResponse languageTestScoreStatus = scoreService.getLanguageTestScoreStatus(siteUser);
return ResponseEntity.ok(languageTestScoreStatus);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.solidconnection.score.dto;

import com.example.solidconnection.application.domain.Gpa;

public record GpaResponse(
double gpa,
double gpaCriteria,
String gpaReportUrl
) {
public static GpaResponse from(Gpa gpa) {
return new GpaResponse(
gpa.getGpa(),
gpa.getGpaCriteria(),
gpa.getGpaReportUrl()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import jakarta.validation.constraints.NotNull;

public record GpaScoreRequest(

@NotNull(message = "학점을 입력해주세요.")
Double gpa,

Expand Down
Loading