Skip to content

Commit

Permalink
v2.1.0 (#605)
Browse files Browse the repository at this point in the history
  • Loading branch information
uwoobeat authored Aug 11, 2024
2 parents 576419f + 94bff51 commit 0dec94d
Show file tree
Hide file tree
Showing 88 changed files with 1,520 additions and 326 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/develop_build_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:

# Redis 컨테이너 실행
- name: Start containers
run: docker-compose -f ./docker-compose-test.yaml up -d
run: docker compose -f docker-compose-test.yaml up -d

# Gradle 빌드
- name: Setup Gradle
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/production_build_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ jobs:
java-version: ${{ matrix.java-version }}
distribution: ${{ matrix.distribution }}

# Redis 컨테이너 실행
- name: Start containers
run: docker compose -f docker-compose-test.yaml up -d

# Gradlew 실행 허용
- name: Run chmod to make gradlew executable
run: chmod +x ./gradlew

# Redis 컨테이너 실행
- name: Start containers
run: docker-compose -f ./docker-compose-test.yaml up -d

# Gradle 빌드
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pull_request_gradle_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:

# Redis 컨테이너 실행
- name: Start containers
run: docker-compose -f ./docker-compose-test.yaml up -d
run: docker compose -f docker-compose-test.yaml up -d

- name: Setup Gradle
id: gradle
Expand Down
4 changes: 3 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ dependencies {

// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
testImplementation 'org.testcontainers:testcontainers'

// Querydsl
implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
Expand Down Expand Up @@ -93,6 +92,9 @@ dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
implementation 'io.github.openfeign:feign-jackson'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'

// Github
implementation 'org.kohsuke:github-api:1.323'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.gdschongik.gdsc.domain.auth.api;

import static com.gdschongik.gdsc.global.common.constant.JwtConstant.Constants.*;

import com.gdschongik.gdsc.global.util.CookieUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Auth", description = "인증 API입니다.")
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {

private final CookieUtil cookieUtil;

@Operation(summary = "로그아웃", description = "현재 엑세스 토큰 및 리프레시 토큰 쿠키를 만료시킵니다.")
@GetMapping("/logout")
public ResponseEntity<Void> logout(
@CookieValue(ACCESS_TOKEN_COOKIE_NAME) Cookie accessToken,
@CookieValue(REFRESH_TOKEN_COOKIE_NAME) Cookie refreshToken,
HttpServletResponse response) {
cookieUtil.deleteCookie(accessToken, response);
cookieUtil.deleteCookie(refreshToken, response);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
import com.gdschongik.gdsc.domain.auth.domain.RefreshToken;
import com.gdschongik.gdsc.domain.auth.dto.AccessTokenDto;
import com.gdschongik.gdsc.domain.auth.dto.RefreshTokenDto;
import com.gdschongik.gdsc.domain.member.domain.MemberManageRole;
import com.gdschongik.gdsc.domain.member.domain.MemberRole;
import com.gdschongik.gdsc.domain.member.domain.MemberStudyRole;
import com.gdschongik.gdsc.global.security.MemberAuthInfo;
import com.gdschongik.gdsc.global.util.JwtUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
Expand All @@ -22,8 +26,8 @@ public class JwtService {
private final JwtUtil jwtUtil;
private final RefreshTokenRepository refreshTokenRepository;

public AccessTokenDto createAccessToken(Long memberId, MemberRole memberRole) {
return jwtUtil.generateAccessToken(memberId, memberRole);
public AccessTokenDto createAccessToken(MemberAuthInfo authInfo) {
return jwtUtil.generateAccessToken(authInfo);
}

public RefreshTokenDto createRefreshToken(Long memberId) {
Expand Down Expand Up @@ -86,9 +90,16 @@ public AccessTokenDto reissueAccessTokenIfExpired(String accessTokenValue) {
jwtUtil.parseAccessToken(accessTokenValue);
return null;
} catch (ExpiredJwtException e) {
Long memberId = Long.parseLong(e.getClaims().getSubject());
MemberRole memberRole = MemberRole.valueOf(e.getClaims().get(TOKEN_ROLE_NAME, String.class));
return createAccessToken(memberId, memberRole);
Claims claims = e.getClaims();

Long memberId = Long.parseLong(claims.getSubject());
MemberRole memberRole = MemberRole.valueOf(claims.get(TOKEN_ROLE_NAME, String.class));
MemberManageRole memberManageRole =
MemberManageRole.valueOf(claims.get(TOKEN_MANAGE_ROLE_NAME, String.class));
MemberStudyRole memberStudyRole = MemberStudyRole.valueOf(claims.get(TOKEN_STUDY_ROLE_NAME, String.class));
var authInfo = new MemberAuthInfo(memberId, memberRole, memberManageRole, memberStudyRole);

return createAccessToken(authInfo);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package com.gdschongik.gdsc.domain.auth.dto;

import com.gdschongik.gdsc.domain.member.domain.MemberRole;
import com.gdschongik.gdsc.global.security.MemberAuthInfo;

public record AccessTokenDto(Long memberId, MemberRole memberRole, String tokenValue) {}
public record AccessTokenDto(MemberAuthInfo authInfo, String tokenValue) {}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
@Getter
@AllArgsConstructor
public enum RequirementStatus {
PENDING("PENDING"),
UNSATISFIED("UNSATISFIED"),
SATISFIED("SATISFIED");

private final String value;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.gdschongik.gdsc.domain.discord.application.handler;

import static com.gdschongik.gdsc.global.common.constant.DiscordConstant.*;

import com.gdschongik.gdsc.domain.member.domain.MemberDemotedToAssociateEvent;
import com.gdschongik.gdsc.global.util.DiscordUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Role;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class MemberDiscordRoleRevokeHandler implements SpringEventHandler {

private final DiscordUtil discordUtil;

@Override
public void delegate(Object context) {
MemberDemotedToAssociateEvent event = (MemberDemotedToAssociateEvent) context;
Guild guild = discordUtil.getCurrentGuild();
Member member = discordUtil.getMemberById(event.discordId());
Role role = discordUtil.findRoleByName(MEMBER_ROLE_NAME);

guild.removeRoleFromMember(member, role).queue();

log.info(
"[MemberDiscordRoleRevokeHandler] 디스코드 서버 정회원 역할 제거 완료: memberId={}, discordId={}",
event.memberId(),
event.discordId());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.gdschongik.gdsc.domain.discord.application.listener;

import static com.gdschongik.gdsc.global.common.constant.DiscordConstant.*;

import com.gdschongik.gdsc.domain.discord.application.handler.DiscordIdBatchCommandHandler;
import lombok.RequiredArgsConstructor;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
Expand All @@ -15,6 +17,8 @@ public class DiscordIdBatchCommandListener extends ListenerAdapter {
private final DiscordIdBatchCommandHandler discordIdBatchCommandHandler;

public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event) {
discordIdBatchCommandHandler.delegate(event);
if (event.getName().equals(COMMAND_NAME_BATCH_DISCORD_ID)) {
discordIdBatchCommandHandler.delegate(event);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.gdschongik.gdsc.domain.discord.application.listener;

import com.gdschongik.gdsc.domain.discord.application.handler.MemberDiscordRoleRevokeHandler;
import com.gdschongik.gdsc.domain.member.domain.MemberDemotedToAssociateEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Slf4j
@Component
@RequiredArgsConstructor
public class MemberDemotedToAssociateEventListener {

private final MemberDiscordRoleRevokeHandler memberDiscordRoleRevokeHandler;

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void demoteMemberToAssociate(MemberDemotedToAssociateEvent event) {
log.info(
"[MemberDemotedToAssociateEventListener] 회원 준회원 강등 이벤트 수신: memberId={}, discordId={}",
event.memberId(),
event.discordId());
memberDiscordRoleRevokeHandler.delegate(event);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import static com.gdschongik.gdsc.global.exception.ErrorCode.*;

import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.member.domain.MemberRole;
import com.gdschongik.gdsc.domain.member.domain.MemberManageRole;
import com.gdschongik.gdsc.global.annotation.DomainService;
import com.gdschongik.gdsc.global.exception.CustomException;

Expand Down Expand Up @@ -32,7 +32,7 @@ public void validateVerifyDiscordCode(
}

public void validateAdminPermission(Member currentMember) {
if (!currentMember.getRole().equals(MemberRole.ADMIN)) {
if (!currentMember.getManageRole().equals(MemberManageRole.ADMIN)) {
throw new CustomException(INVALID_ROLE);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public UnivVerificationStatus determineStatus(
} else {
return univEmailVerification.isPresent()
? UnivVerificationStatus.IN_PROGRESS
: UnivVerificationStatus.PENDING;
: UnivVerificationStatus.UNSATISFIED;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.gdschongik.gdsc.domain.member.application.AdminMemberService;
import com.gdschongik.gdsc.domain.member.application.OnboardingMemberService;
import com.gdschongik.gdsc.domain.member.application.TestMemberService;
import com.gdschongik.gdsc.domain.member.dto.request.MemberTokenRequest;
import com.gdschongik.gdsc.domain.member.dto.response.MemberTokenResponse;
import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -17,9 +18,17 @@
@RequiredArgsConstructor
public class TestMemberController {

private final TestMemberService testMemberService;
private final OnboardingMemberService onboardingMemberService;
private final AdminMemberService adminMemberService;

@Operation(summary = "게스트 회원 생성", description = "테스트용 API입니다. 깃허브 핸들명을 입력받아 임시 회원을 생성합니다.")
@PostMapping
public ResponseEntity<Void> createTemporaryMember(@RequestParam("handle") String githubHandle) {
testMemberService.createTestMember(githubHandle);
return ResponseEntity.ok().build();
}

@Operation(summary = "임시 토큰 생성", description = "테스트용 API입니다. oauth_id를 입력받아 해당하는 유저의 토큰을 생성합니다.")
@PostMapping("/token")
public ResponseEntity<MemberTokenResponse> createTemporaryToken(@Valid @RequestBody MemberTokenRequest request) {
Expand All @@ -30,7 +39,7 @@ public ResponseEntity<MemberTokenResponse> createTemporaryToken(@Valid @RequestB
@Operation(summary = "게스트로 강등", description = "테스트용 API입니다. 현재 멤버 역할을 게스트로 강등시키기 위해 사용합니다.")
@PatchMapping("/demotion")
public ResponseEntity<Void> demoteToGuest() {
adminMemberService.demoteToGuestAndRegularRequirementToPending();
adminMemberService.demoteToGuestAndRegularRequirementToUnsatisfied();
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,8 @@ public void demoteAllRegularMembersToAssociate(MemberDemoteRequest request) {
regularMembers.stream().map(Member::getId).toList());
}

/**
* 정회원 조건 PENDING으로 변경, 준회원 조건 PENDING으로 변경
*/
@Transactional
public void demoteToGuestAndRegularRequirementToPending() {
public void demoteToGuestAndRegularRequirementToUnsatisfied() {
validateProfile();
Member member = memberUtil.getCurrentMember();
member.demoteToGuest();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,23 @@ public void advanceMemberToRegularByMembership(Long membershipId) {
log.info("[CommonMemberService] 정회원 승급 완료: memberId={}", member.getId());
}
}

/**
* 이벤트 핸들러에서 사용되므로, `@Transactional` 을 사용하지 않습니다.
*/
public void demoteMemberToAssociateByMembership(Long membershipId) {
Membership membership = membershipRepository
.findById(membershipId)
.orElseThrow(() -> new CustomException(MEMBERSHIP_NOT_FOUND));

Member member = memberRepository
.findById(membership.getMember().getId())
.orElseThrow(() -> new CustomException(MEMBER_NOT_FOUND));

member.demoteToAssociate();

memberRepository.save(member);

log.info("[CommonMemberService] 준회원 강등 완료: memberId={}", member.getId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.gdschongik.gdsc.domain.recruitment.application.OnboardingRecruitmentService;
import com.gdschongik.gdsc.domain.recruitment.domain.RecruitmentRound;
import com.gdschongik.gdsc.global.exception.CustomException;
import com.gdschongik.gdsc.global.security.MemberAuthInfo;
import com.gdschongik.gdsc.global.util.EnvironmentUtil;
import com.gdschongik.gdsc.global.util.MemberUtil;
import java.util.Optional;
Expand Down Expand Up @@ -70,15 +71,16 @@ public MemberBasicInfoResponse getMemberBasicInfo() {

public MemberDashboardResponse getDashboard() {
final Member member = memberUtil.getCurrentMember();
final RecruitmentRound currentRecruitmentRound = onboardingRecruitmentService.findCurrentRecruitmentRound();
final Optional<Membership> myMembership = membershipService.findMyMembership(member, currentRecruitmentRound);
final Optional<UnivEmailVerification> univEmailVerification =
univEmailVerificationService.getUnivEmailVerificationFromRedis(member.getId());
UnivVerificationStatus univVerificationStatus =
emailVerificationStatusService.determineStatus(member, univEmailVerification);
Optional<RecruitmentRound> currentRecruitmentRound = onboardingRecruitmentService.findCurrentRecruitmentRound();
Optional<Membership> myMembership = currentRecruitmentRound.flatMap(
recruitmentRound -> membershipService.findMyMembership(member, recruitmentRound));

return MemberDashboardResponse.of(
member, univVerificationStatus, currentRecruitmentRound, myMembership.orElse(null));
member, univVerificationStatus, currentRecruitmentRound.orElse(null), myMembership.orElse(null));
}

public MemberTokenResponse createTemporaryToken(MemberTokenRequest request) {
Expand All @@ -88,7 +90,7 @@ public MemberTokenResponse createTemporaryToken(MemberTokenRequest request) {
.findByOauthId(request.oauthId())
.orElseThrow(() -> new CustomException(MEMBER_NOT_FOUND));

AccessTokenDto accessTokenDto = jwtService.createAccessToken(member.getId(), member.getRole());
AccessTokenDto accessTokenDto = jwtService.createAccessToken(MemberAuthInfo.from(member));
RefreshTokenDto refreshTokenDto = jwtService.createRefreshToken(member.getId());

return new MemberTokenResponse(accessTokenDto.tokenValue(), refreshTokenDto.tokenValue());
Expand Down
Loading

0 comments on commit 0dec94d

Please sign in to comment.