Skip to content

Commit

Permalink
[BE] feat: 그룹 접근 코드 해싱 적용 (#489)
Browse files Browse the repository at this point in the history
* refactor: v2 잔재 제거

* feat: 해싱 인코더

* feat: 해싱 코드 생성 및 검증

* feat: 그룹 접근 코드 해싱 적용 및 검증
  • Loading branch information
donghoony authored Aug 21, 2024
1 parent 069306d commit 26f847e
Show file tree
Hide file tree
Showing 22 changed files with 212 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import reviewme.global.exception.DataInconsistencyException;
import reviewme.global.exception.FieldErrorResponse;
import reviewme.global.exception.NotFoundException;
import reviewme.global.exception.UnauthorizedException;
import reviewme.global.exception.UnexpectedRequestException;

@Slf4j
Expand All @@ -45,6 +46,11 @@ public ProblemDetail handleUnexpectedRequestException(UnexpectedRequestException
return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getErrorMessage());
}

@ExceptionHandler(UnauthorizedException.class)
public ProblemDetail handleUnauthorizedException(UnauthorizedException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, ex.getErrorMessage());
}

@ExceptionHandler(DataInconsistencyException.class)
public ProblemDetail handleDataConsistencyException(DataInconsistencyException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, ex.getErrorMessage());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package reviewme.global.exception;

public abstract class UnauthorizedException extends ReviewMeException {

protected UnauthorizedException(String errorMessage) {
super(errorMessage);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,4 @@ public interface ReviewRepository extends JpaRepository<Review, Long> {
List<Review> findReceivedReviewsByGroupId(long reviewGroupId);

Optional<Review> findByIdAndReviewGroupId(long reviewId, long reviewGroupId);

@Query(value = """
SELECT r.* FROM review r
INNER JOIN review_group rg
ON rg.id = r.review_group_id
WHERE r.id = :reviewId
AND rg.review_request_code = :reviewRequestCode
AND rg.group_access_code = :groupAccessCode
""", nativeQuery = true)
Optional<Review> findByIdAndCodes(long reviewId, String reviewRequestCode, String groupAccessCode);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
import reviewme.review.domain.Review;
import reviewme.review.domain.TextAnswer;
import reviewme.review.domain.TextAnswers;
import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException;
import reviewme.review.repository.ReviewRepository;
import reviewme.review.service.dto.response.detail.OptionGroupAnswerResponse;
import reviewme.review.service.dto.response.detail.OptionItemAnswerResponse;
import reviewme.review.service.dto.response.detail.QuestionAnswerResponse;
import reviewme.review.service.dto.response.detail.SectionAnswerResponse;
import reviewme.review.service.dto.response.detail.TemplateAnswerResponse;
import reviewme.review.service.exception.ReviewGroupNotFoundByReviewException;
import reviewme.review.service.exception.ReviewNotFoundByIdAndCodesException;
import reviewme.review.service.exception.ReviewGroupUnauthorizedException;
import reviewme.review.service.exception.ReviewNotFoundByIdAndGroupException;
import reviewme.reviewgroup.domain.ReviewGroup;
import reviewme.reviewgroup.repository.ReviewGroupRepository;
import reviewme.template.domain.Section;
Expand All @@ -42,11 +43,13 @@ public class ReviewDetailLookupService {
private final OptionGroupRepository optionGroupRepository;

public TemplateAnswerResponse getReviewDetail(long reviewId, String reviewRequestCode, String groupAccessCode) {
Review review = reviewRepository.findByIdAndCodes(reviewId, reviewRequestCode, groupAccessCode)
.orElseThrow(() -> new ReviewNotFoundByIdAndCodesException(reviewId, reviewRequestCode, groupAccessCode));

ReviewGroup reviewGroup = reviewGroupRepository.findById(review.getReviewGroupId())
.orElseThrow(() -> new ReviewGroupNotFoundByReviewException(review.getId(), review.getReviewGroupId()));
ReviewGroup reviewGroup = reviewGroupRepository.findByReviewRequestCode(reviewRequestCode)
.orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode));
if (!reviewGroup.matchesGroupAccessCode(groupAccessCode)) {
throw new ReviewGroupUnauthorizedException(reviewGroup.getId());
}
Review review = reviewRepository.findByIdAndReviewGroupId(reviewId, reviewGroup.getId())
.orElseThrow(() -> new ReviewNotFoundByIdAndGroupException(reviewId, reviewGroup.getId()));

long templateId = review.getTemplateId();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public class ReviewPreviewGenerator {

private static final int PREVIEW_LENGTH = 150;

public String generatePreview2(List<TextAnswer> reviewTextAnswers) {
public String generatePreview(List<TextAnswer> reviewTextAnswers) {
if (reviewTextAnswers.isEmpty()) {
return "";
}
Expand Down
14 changes: 9 additions & 5 deletions backend/src/main/java/reviewme/review/service/ReviewService.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
import reviewme.question.domain.OptionType;
import reviewme.question.repository.OptionItemRepository;
import reviewme.review.domain.Review;
import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException;
import reviewme.review.repository.ReviewRepository;
import reviewme.review.service.dto.response.list.ReceivedReviewCategoryResponse;
import reviewme.review.service.dto.response.list.ReceivedReviewResponse;
import reviewme.review.service.dto.response.list.ReceivedReviewsResponse;
import reviewme.review.service.exception.ReviewGroupNotFoundByCodesException;
import reviewme.review.service.exception.ReviewGroupUnauthorizedException;
import reviewme.reviewgroup.domain.ReviewGroup;
import reviewme.reviewgroup.repository.ReviewGroupRepository;

Expand All @@ -28,9 +29,12 @@ public class ReviewService {

@Transactional(readOnly = true)
public ReceivedReviewsResponse findReceivedReviews(String reviewRequestCode, String groupAccessCode) {
ReviewGroup reviewGroup = reviewGroupRepository
.findByReviewRequestCodeAndGroupAccessCode_Code(reviewRequestCode, groupAccessCode)
.orElseThrow(() -> new ReviewGroupNotFoundByCodesException(reviewRequestCode, groupAccessCode));
ReviewGroup reviewGroup = reviewGroupRepository.findByReviewRequestCode(reviewRequestCode)
.orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(reviewRequestCode));

if (!reviewGroup.matchesGroupAccessCode(groupAccessCode)) {
throw new ReviewGroupUnauthorizedException(reviewGroup.getId());
}

List<ReceivedReviewResponse> reviewResponses =
reviewRepository.findReceivedReviewsByGroupId(reviewGroup.getId())
Expand All @@ -52,7 +56,7 @@ private ReceivedReviewResponse createReceivedReviewResponse(Review review) {
return new ReceivedReviewResponse(
review.getId(),
review.getCreatedAt().toLocalDate(),
reviewPreviewGenerator.generatePreview2(review.getTextAnswers()),
reviewPreviewGenerator.generatePreview(review.getTextAnswers()),
categoryResponses
);
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package reviewme.review.service.exception;

import lombok.extern.slf4j.Slf4j;
import reviewme.global.exception.UnauthorizedException;

@Slf4j
public class ReviewGroupUnauthorizedException extends UnauthorizedException {

public ReviewGroupUnauthorizedException(long reviewGroupId) {
super("리뷰를 확인할 권한이 없어요.");
log.info("Group access code mismatch on review group: {}", reviewGroupId);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package reviewme.review.service.exception;

import lombok.extern.slf4j.Slf4j;
import reviewme.global.exception.NotFoundException;

@Slf4j
public class ReviewNotFoundByIdAndGroupException extends NotFoundException {

public ReviewNotFoundByIdAndGroupException(long reviewId, long reviewGroupId) {
super("리뷰를 찾을 수 없어요");
log.info("Review not found from group - reviewGroupId: {}, reviewId: {}", reviewGroupId, reviewId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package reviewme.review.service.exception;

import lombok.extern.slf4j.Slf4j;
import reviewme.global.exception.NotFoundException;

@Slf4j
public class ReviewNotFoundException extends NotFoundException {

public ReviewNotFoundException(String reviewRequestCode, long reviewId) {
super("리뷰가 존재하지 않아요.");
log.info("Review not found: reviewRequestCode: {}, reviewId: {}", reviewRequestCode, reviewId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import reviewme.reviewgroup.domain.exception.InvalidGroupAccessCodeFormatException;
import reviewme.util.Encoder;

@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
Expand All @@ -21,7 +22,7 @@ public class GroupAccessCode {

public GroupAccessCode(String code) {
validateGroupAccessCode(code);
this.code = code;
this.code = Encoder.encode(code);
}

private void validateGroupAccessCode(String groupAccessCode) {
Expand All @@ -30,5 +31,8 @@ private void validateGroupAccessCode(String groupAccessCode) {
throw new InvalidGroupAccessCodeFormatException(groupAccessCode);
}
}
}

public boolean matches(String groupAccessCode) {
return code.equals(Encoder.encode(groupAccessCode));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ private void validateProjectNameLength(String projectName) {
}
}

public boolean matchesGroupAccessCode(String code) {
return groupAccessCode.matches(code);
}

public String getGroupAccessCode() {
return groupAccessCode.getCode();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,5 @@ public interface ReviewGroupRepository extends JpaRepository<ReviewGroup, Long>

Optional<ReviewGroup> findByReviewRequestCode(String reviewRequestCode);

Optional<ReviewGroup> findByReviewRequestCodeAndGroupAccessCode_Code(String reviewRequestCode, String groupAccessCode);

boolean existsByReviewRequestCode(String reviewRequestCode);

boolean existsByReviewRequestCodeAndGroupAccessCode_Code(String reviewRequestCode, String groupAccessCode);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reviewme.review.domain.exception.ReviewGroupNotFoundByReviewRequestCodeException;
import reviewme.reviewgroup.domain.ReviewGroup;
import reviewme.reviewgroup.repository.ReviewGroupRepository;
import reviewme.reviewgroup.service.dto.CheckValidAccessRequest;
Expand All @@ -26,7 +27,6 @@ public ReviewGroupCreationResponse createReviewGroup(ReviewGroupCreationRequest
reviewRequestCode = randomCodeGenerator.generate(REVIEW_REQUEST_CODE_LENGTH);
} while (reviewGroupRepository.existsByReviewRequestCode(reviewRequestCode));


ReviewGroup reviewGroup = reviewGroupRepository.save(
new ReviewGroup(request.revieweeName(), request.projectName(), reviewRequestCode, request.groupAccessCode())
);
Expand All @@ -35,9 +35,10 @@ public ReviewGroupCreationResponse createReviewGroup(ReviewGroupCreationRequest

@Transactional(readOnly = true)
public CheckValidAccessResponse checkGroupAccessCode(CheckValidAccessRequest request) {
boolean hasAccess = reviewGroupRepository.existsByReviewRequestCodeAndGroupAccessCode_Code(
request.reviewRequestCode(), request.groupAccessCode()
);
ReviewGroup reviewGroup = reviewGroupRepository.findByReviewRequestCode(request.reviewRequestCode())
.orElseThrow(() -> new ReviewGroupNotFoundByReviewRequestCodeException(request.reviewRequestCode()));

boolean hasAccess = reviewGroup.matchesGroupAccessCode(request.groupAccessCode());
return new CheckValidAccessResponse(hasAccess);
}
}
32 changes: 32 additions & 0 deletions backend/src/main/java/reviewme/util/Encoder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package reviewme.util;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class Encoder {

private static final String SHA_256 = "SHA-256";

private Encoder() {
}

public static String encode(String code) {
try {
MessageDigest messageDigest = MessageDigest.getInstance(SHA_256);
byte[] digest = messageDigest.digest(code.getBytes(UTF_8));
return formatHexadecimal(digest);
} catch (NoSuchAlgorithmException e) {
throw new EncoderAlgorithmInitializationException(SHA_256);
}
}

private static String formatHexadecimal(byte[] bytes) {
StringBuilder builder = new StringBuilder();
for (byte b : bytes) {
builder.append("%02x".formatted(b));
}
return builder.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package reviewme.util;

import lombok.extern.slf4j.Slf4j;
import reviewme.global.exception.ReviewMeException;

@Slf4j
public class EncoderAlgorithmInitializationException extends ReviewMeException {

public EncoderAlgorithmInitializationException(String algorithm) {
super("서버 내부에 문제가 발생했습니다. 잠시 후 다시 시도해주세요.");
log.error("Failed to initialize encoder: Algorithm not found: {}", algorithm, this);
}
}
Loading

0 comments on commit 26f847e

Please sign in to comment.