Skip to content
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

fix: 미션 인증 유효성 검사 추가 #30

Merged
merged 24 commits into from
Aug 4, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f43517e
feat: 내가 참여중인 미션 조회 API 구현
songyi00 Aug 1, 2024
ef86f0e
refactor: dto 패키지 정리
songyi00 Aug 1, 2024
8d9dae1
fix: 미션 인증 시 요일 검증 추가
kimyu0218 Aug 3, 2024
16d11e0
feat: 장기말 타입 추가
kimyu0218 Aug 3, 2024
449ce1e
feat: 구글을 위한 자체 토큰 생성 추가
kimyu0218 Aug 3, 2024
35c1572
remove: identity token 필드 제거
kimyu0218 Aug 3, 2024
5b5b730
refactor: identity token 자체 생성
kimyu0218 Aug 3, 2024
05c3888
fix: 충돌 해결
kimyu0218 Aug 3, 2024
0ebfad6
fix: 충돌 해결
kimyu0218 Aug 3, 2024
6299ef2
feat: 에러 코드 추가
kimyu0218 Aug 3, 2024
4525237
rename: command -> query
kimyu0218 Aug 3, 2024
1b536de
refactor: 서비스 로직 단순화
kimyu0218 Aug 3, 2024
e7a1871
refactor: 인터페이스 및 구현체 분리
kimyu0218 Aug 3, 2024
14c1687
refactor: 서비스 로직 단순화
kimyu0218 Aug 3, 2024
712cad2
remove: 불필요한 코드 제거
kimyu0218 Aug 3, 2024
0d1b5ef
rename: bean 이름 변경
kimyu0218 Aug 4, 2024
c073ab6
remove: 불필요한 dto 제거
kimyu0218 Aug 4, 2024
f75280c
fix: 서비스 및 레포지토리 에러 픽스
kimyu0218 Aug 4, 2024
7c653de
feat: BadRequest 400 던지도록 변경
kimyu0218 Aug 4, 2024
5c79b2a
fix: java.time.DayOfWeek 호환 문제 해결
kimyu0218 Aug 4, 2024
5c2631b
test: 미션 인증 테스트 작성
kimyu0218 Aug 4, 2024
231dd75
test: 중복 인증 테스트케이스 추가
kimyu0218 Aug 4, 2024
d21be66
refactor: DayOfWeek 검증 방식 변경
kimyu0218 Aug 4, 2024
a4ba8a4
docs: swagger 수정
kimyu0218 Aug 4, 2024
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
Expand Up @@ -4,6 +4,7 @@
import com.nexters.goalpanzi.application.auth.dto.request.GoogleLoginCommand;
import com.nexters.goalpanzi.application.auth.dto.response.LoginResponse;
import com.nexters.goalpanzi.application.auth.dto.response.TokenResponse;
import com.nexters.goalpanzi.application.auth.google.GoogleIdentityToken;
import com.nexters.goalpanzi.common.jwt.Jwt;
import com.nexters.goalpanzi.common.jwt.JwtProvider;
import com.nexters.goalpanzi.domain.auth.repository.RefreshTokenRepository;
Expand Down Expand Up @@ -32,7 +33,8 @@ public LoginResponse appleOAuthLogin(final AppleLoginCommand command) {
}

public LoginResponse googleOAuthLogin(final GoogleLoginCommand command) {
SocialUserInfo socialUserInfo = new SocialUserInfo(command.identityToken(), command.email());
SocialUserInfo socialUserInfo = new SocialUserInfo(
GoogleIdentityToken.generate(command.email()), command.email());

return socialLogin(socialUserInfo, SocialType.GOOGLE);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import jakarta.validation.constraints.NotEmpty;

public record GoogleLoginCommand(
@NotEmpty String identityToken,
@NotEmpty String email
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.nexters.goalpanzi.application.auth.google;

import com.nexters.goalpanzi.domain.member.SocialType;
import com.nexters.goalpanzi.exception.BaseException;
import com.nexters.goalpanzi.exception.ErrorCode;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

public class GoogleIdentityToken {

private static String SHA_256 = "SHA-256";

public static String generate(final String email) {
return generateHash(email + "__" + SocialType.GOOGLE);
}

private static String generateHash(final String input) {
try {
return Base64.getUrlEncoder().withoutPadding().encodeToString(
MessageDigest.getInstance(SHA_256).digest(input.getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException e) {
throw new BaseException(ErrorCode.FAILED_TO_GENERATE_HASH);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

import com.nexters.goalpanzi.application.mission.dto.request.CreateMissionVerificationCommand;
import com.nexters.goalpanzi.application.mission.dto.request.MissionVerificationCommand;
import com.nexters.goalpanzi.application.mission.dto.response.MissionVerificationResponse;
import com.nexters.goalpanzi.application.mission.dto.response.MissionsResponse;
import com.nexters.goalpanzi.application.mission.dto.request.MyMissionVerificationCommand;
import com.nexters.goalpanzi.application.mission.dto.response.MissionVerificationResponse;
import com.nexters.goalpanzi.domain.member.Member;
import com.nexters.goalpanzi.domain.member.repository.MemberRepository;
import com.nexters.goalpanzi.domain.mission.Mission;
Expand All @@ -22,7 +21,6 @@
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
Expand All @@ -43,72 +41,78 @@ public class MissionVerificationService {
@Transactional(readOnly = true)
public List<MissionVerificationResponse> getVerifications(final MissionVerificationCommand command) {
LocalDate date = command.date() != null ? command.date() : LocalDate.now();
Member member =
memberRepository.findById(command.memberId())
.orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_MEMBER));
List<MissionMember> missionMembers = missionMemberRepository.findAllByMissionId(command.missionId());
Member member = getMember(command.memberId());
List<MissionVerification> verifications = missionVerificationRepository.findAllByMissionIdAndDate(command.missionId(), date);

Map<Long, MissionVerification> verificationMap = verifications.stream()
.collect(Collectors.toMap(v -> v.getMember().getId(), v -> v));

return missionMembers.stream()
.map(m -> {
MissionVerification v = verificationMap.get(m.getMember().getId());
return v != null
? MissionVerificationResponse.verified(m.getMember(), v)
: MissionVerificationResponse.notVerified(m.getMember());
})
return getMissionMembers(command.missionId()).stream()
.map(m -> convertToVerificationResponse(m, verificationMap.get(m.getMember().getId())))
.sorted(Comparator.comparing((MissionVerificationResponse r) -> r.nickname().equals(member.getNickname())).reversed()
.thenComparing(MissionVerificationResponse::verifiedAt, Comparator.nullsLast(Comparator.reverseOrder())))
.collect(Collectors.toList());
}

public MissionVerificationResponse getMyVerification(final MyMissionVerificationCommand command) {
kimyu0218 marked this conversation as resolved.
Show resolved Hide resolved
Member member =
memberRepository.findById(command.memberId())
.orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_MEMBER));
MissionVerification verification =
missionVerificationRepository.findByMemberIdAndMissionIdAndBoardNumber(command.memberId(), command.missionId(), command.number())
kimyu0218 marked this conversation as resolved.
Show resolved Hide resolved
.orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_VERIFICATION));

return MissionVerificationResponse.verified(member, verification);
return MissionVerificationResponse.verified(getMember(command.memberId()), verification);
}

@Transactional
public void createVerification(final CreateMissionVerificationCommand command) {
Member member =
memberRepository.findById(command.memberId())
.orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_MEMBER));
Mission mission =
missionRepository.findById(command.missionId())
.orElseThrow(() -> new NotFoundException("TODO"));
MissionMember missionMember =
missionMemberRepository.findByMemberIdAndMissionId(command.memberId(), command.missionId())
.orElseThrow(() -> new NotFoundException("TODO"));
Mission mission = missionRepository.getMission(command.missionId());

checkVerificationValidation(command.memberId(), command.missionId(), mission, missionMember);
checkVerificationValidation(command.memberId(), mission, missionMember);

String imageUrl = objectStorageManager.uploadFile(command.imageFile());
kimyu0218 marked this conversation as resolved.
Show resolved Hide resolved
missionVerificationRepository.save(new MissionVerification(member, mission, imageUrl));
missionVerificationRepository.save(new MissionVerification(getMember(command.memberId()), mission, imageUrl));
missionMember.verify();
}

private void checkVerificationValidation(final Long memberId, final Long missionId, final Mission mission, final MissionMember missionMember) {
private MissionVerificationResponse convertToVerificationResponse(final MissionMember missionMember, final MissionVerification verification) {
return verification != null
? MissionVerificationResponse.verified(missionMember.getMember(), verification)
: MissionVerificationResponse.notVerified(missionMember.getMember());
}

private void checkVerificationValidation(final Long memberId, final Mission mission, final MissionMember missionMember) {
if (isCompletedMission(mission, missionMember)) {
throw new BadRequestException(ErrorCode.ALREADY_COMPLETED_MISSION);
}
if (isDuplicatedVerification(memberId, missionId)) {
LocalDate today = LocalDate.now();
if (isDuplicatedVerification(memberId, mission.getId(), today)) {
throw new BadRequestException(ErrorCode.DUPLICATE_VERIFICATION);
}
if (!isVerificationDay(mission, today)) {
throw new BadRequestException(ErrorCode.NOT_VERIFICATION_DAY);
}
}

private boolean isCompletedMission(final Mission mission, final MissionMember missionMember) {
return missionMember.getVerificationCount() >= mission.getBoardCount();
}

private boolean isDuplicatedVerification(final Long memberId, final Long missionId) {
LocalDate today = LocalDateTime.now().toLocalDate();
private boolean isVerificationDay(final Mission mission, final LocalDate today) {
kimyu0218 marked this conversation as resolved.
Show resolved Hide resolved
return mission.getMissionDays().contains(today.getDayOfWeek());
}

private boolean isDuplicatedVerification(final Long memberId, final Long missionId, final LocalDate today) {
return missionVerificationRepository.findByMemberIdAndMissionIdAndDate(memberId, missionId, today).isPresent();
}

private Member getMember(final Long memberId) {
kimyu0218 marked this conversation as resolved.
Show resolved Hide resolved
return memberRepository.findById(memberId)
.orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_MEMBER));
}

private List<MissionMember> getMissionMembers(final Long missionId) {
return missionMemberRepository.findAllByMissionId(missionId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ public record MissionResponse(
@Schema(description = "목표 행동", requiredMode = Schema.RequiredMode.REQUIRED)
String description
) {
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.nexters.goalpanzi.application.mission.dto.response;

import com.nexters.goalpanzi.domain.member.CharacterType;
import com.nexters.goalpanzi.domain.member.Member;
import com.nexters.goalpanzi.domain.mission.MissionVerification;
import io.swagger.v3.oas.annotations.media.Schema;
Expand All @@ -10,21 +11,19 @@
public record MissionVerificationResponse(
@Schema(description = "닉네임", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty String nickname,

// TODO 캐릭터 이미지

@Schema(description = "장기말 타입", requiredMode = Schema.RequiredMode.REQUIRED)
CharacterType characterType,
@Schema(description = "인증 이미지 URL", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
String imageUrl,

@Schema(description = "인증 시간", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
LocalDateTime verifiedAt
) {

public static MissionVerificationResponse verified(Member member, MissionVerification verification) {
return new MissionVerificationResponse(member.getNickname(), verification.getImageUrl(), verification.getCreatedAt());
return new MissionVerificationResponse(member.getNickname(), member.getCharacterType(), verification.getImageUrl(), verification.getCreatedAt());
}

public static MissionVerificationResponse notVerified(Member member) {
return new MissionVerificationResponse(member.getNickname(), "", null);
return new MissionVerificationResponse(member.getNickname(), member.getCharacterType(), "", null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.nexters.goalpanzi.application.member.dto.response.ProfileResponse;
import com.nexters.goalpanzi.domain.member.Member;
import com.nexters.goalpanzi.domain.mission.MissionMember;
import com.nexters.goalpanzi.domain.mission.MissionVerification;
import io.swagger.v3.oas.annotations.media.Schema;

import java.util.List;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,14 @@ public enum ErrorCode {
NOT_FOUND_VERIFICATION("존재하지 않는 미션 인증입니다."),
DUPLICATE_VERIFICATION("이미 인증한 미션이므로 더 이상 인증할 수 없습니다."),
ALREADY_COMPLETED_MISSION("이미 완료된 미션이므로 더 이상 인증할 수 없습니다."),
NOT_VERIFICATION_DAY("인증 일자가 아니므로 인증할 수 없습니다."),
kimyu0218 marked this conversation as resolved.
Show resolved Hide resolved

// FILE UPLOAD
INVALID_FILE("유효하지 않은 파일입니다."),
FILE_UPLOAD_FAILED("파일 업로드에 실패했습니다.");
FILE_UPLOAD_FAILED("파일 업로드에 실패하였습니다."),

// ETC
FAILED_TO_GENERATE_HASH("해시값을 생성하는 데 실패하였습니다.");

private String message;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ ResponseEntity<MissionVerificationResponse> getMyVerification(
@Parameter(hidden = true) @LoginMemberId final Long memberId,
@Schema(description = "미션 아이디", type = "integer", format = "int64", requiredMode = Schema.RequiredMode.REQUIRED)
@PathVariable(name = "missionId") final Long missionId,
@Schema(description = "보드판 번호", type = "integer", format = "int32", requiredMode = Schema.RequiredMode.REQUIRED)
@Schema(description = "보드칸 번호", type = "integer", format = "int32", requiredMode = Schema.RequiredMode.REQUIRED)
@PathVariable(name = "number") final Integer number);

@Operation(summary = "미션 인증", description = "미션 인증을 위해 이미지를 업로드합니다.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import java.security.NoSuchAlgorithmException;

import static com.nexters.goalpanzi.fixture.MemberFixture.EMAIL;
import static com.nexters.goalpanzi.fixture.MemberFixture.ID_TOKEN;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.mockito.ArgumentMatchers.any;
Expand Down Expand Up @@ -58,8 +57,8 @@ public class LoginAcceptanceTest extends AcceptanceTest {
}

@Test
void 사용자가_구글_로그인을_정상적으로_한다() {
GoogleLoginCommand request = new GoogleLoginCommand(ID_TOKEN, EMAIL);
void 사용자가_구글_로그인을_정상적으로_한다() {
GoogleLoginCommand request = new GoogleLoginCommand(EMAIL);

LoginResponse actual = RestAssured.given().log().all()
.contentType(MediaType.APPLICATION_JSON_VALUE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

import static com.nexters.goalpanzi.acceptance.AcceptanceStep.*;
import static com.nexters.goalpanzi.fixture.MemberFixture.EMAIL;
import static com.nexters.goalpanzi.fixture.MemberFixture.ID_TOKEN;
import static com.nexters.goalpanzi.fixture.MissionFixture.DESCRIPTION;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
Expand All @@ -22,7 +21,7 @@ public class MissionAcceptanceTest extends AcceptanceTest {

@Test
void 미션을_생성한다() {
LoginResponse loginResponse = 구글_로그인(new GoogleLoginCommand(ID_TOKEN, EMAIL)).as(LoginResponse.class);
LoginResponse loginResponse = 구글_로그인(new GoogleLoginCommand(EMAIL)).as(LoginResponse.class);

CreateMissionRequest request = new CreateMissionRequest(DESCRIPTION, LocalDateTime.now(), LocalDateTime.now().plusDays(5), TimeOfDay.EVERYDAY, List.of(DayOfWeek.FRIDAY), 5);

Expand All @@ -36,7 +35,7 @@ public class MissionAcceptanceTest extends AcceptanceTest {

@Test
void 미션을_조회한다() {
LoginResponse login = 구글_로그인(new GoogleLoginCommand(ID_TOKEN, EMAIL)).as(LoginResponse.class);
LoginResponse login = 구글_로그인(new GoogleLoginCommand(EMAIL)).as(LoginResponse.class);

CreateMissionRequest request = new CreateMissionRequest(DESCRIPTION, LocalDateTime.now(),
LocalDateTime.now().plusDays(5), TimeOfDay.EVERYDAY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

import static com.nexters.goalpanzi.acceptance.AcceptanceStep.*;
import static com.nexters.goalpanzi.fixture.MemberFixture.EMAIL;
import static com.nexters.goalpanzi.fixture.MemberFixture.ID_TOKEN;
import static com.nexters.goalpanzi.fixture.TokenFixture.BEARER;
import static org.assertj.core.api.Assertions.assertThat;

Expand All @@ -26,7 +25,7 @@ public class MissionMemberAcceptanceTest extends AcceptanceTest {

@Test
void 초대코드로_미션에_참여한다() {
LoginResponse login = 구글_로그인(new GoogleLoginCommand(ID_TOKEN, EMAIL)).as(LoginResponse.class);
LoginResponse login = 구글_로그인(new GoogleLoginCommand(EMAIL)).as(LoginResponse.class);
MissionDetailResponse mission = 미션_생성(login.accessToken()).as(MissionDetailResponse.class);

JoinMissionRequest joinRequest = new JoinMissionRequest(mission.invitationCode());
Expand All @@ -41,7 +40,7 @@ public class MissionMemberAcceptanceTest extends AcceptanceTest {

@Test
void 참여하고있는_미션을_조회한다() {
LoginResponse login = 구글_로그인(new GoogleLoginCommand(ID_TOKEN, EMAIL)).as(LoginResponse.class);
LoginResponse login = 구글_로그인(new GoogleLoginCommand(EMAIL)).as(LoginResponse.class);
MissionDetailResponse mission = 미션_생성(login.accessToken()).as(MissionDetailResponse.class);
미션_참여(mission.invitationCode(), login.accessToken());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ class MissionTest {
LocalDateTime.now().plusDays(7),
TimeOfDay.EVERYDAY,
List.of(DayOfWeek.FRIDAY),
BOARD_COUNT
BOARD_COUNT,
InvitationCode.generate()
);

assertAll(
Expand All @@ -42,7 +43,8 @@ class MissionTest {
LocalDateTime.now().plusDays(7),
TimeOfDay.EVERYDAY,
List.of(DayOfWeek.FRIDAY),
0
0,
InvitationCode.generate()
));
}

Expand All @@ -55,7 +57,8 @@ class MissionTest {
LocalDateTime.now().minusDays(7),
TimeOfDay.EVERYDAY,
List.of(DayOfWeek.FRIDAY),
BOARD_COUNT
BOARD_COUNT,
InvitationCode.generate()
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

public class MemberFixture {
public static final Long MEMBER_ID = 1L;
public static final String ID_TOKEN = "token";
public static final String EMAIL = "[email protected]";
public static final String SOCIAL_ID = "12345";
public static final String NICKNAME = "song2";
Expand Down
Loading