diff --git a/src/main/java/clap/server/adapter/inbound/web/auth/ReissueTokenController.java b/src/main/java/clap/server/adapter/inbound/web/auth/ReissueTokenController.java new file mode 100644 index 00000000..8deff65a --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/auth/ReissueTokenController.java @@ -0,0 +1,26 @@ +package clap.server.adapter.inbound.web.auth; + +import clap.server.adapter.inbound.web.dto.auth.ReissueTokenResponse; +import clap.server.application.port.inbound.auth.ReissueTokenUsecase; +import clap.server.common.annotation.architecture.WebAdapter; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; + +@Tag(name = "토큰 재발급") +@WebAdapter +@RequiredArgsConstructor +@RequestMapping("/api/auths") +public class ReissueTokenController { + private final ReissueTokenUsecase reissueTokenUsecase; + + @Operation(summary = "토큰 재발급 API") + @PostMapping("/reissuance") + public ResponseEntity login(@RequestHeader String refreshToken) { + return ResponseEntity.ok(reissueTokenUsecase.reissueToken(refreshToken)); + } +} diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/auth/ReissueTokenResponse.java b/src/main/java/clap/server/adapter/inbound/web/dto/auth/ReissueTokenResponse.java new file mode 100644 index 00000000..128abc3f --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/auth/ReissueTokenResponse.java @@ -0,0 +1,7 @@ +package clap.server.adapter.inbound.web.dto.auth; + +public record ReissueTokenResponse( + String accessToken, + String refreshToken +) { +} diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/notification/FindNotificationListResponse.java b/src/main/java/clap/server/adapter/inbound/web/dto/notification/FindNotificationListResponse.java new file mode 100644 index 00000000..1326e0f2 --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/notification/FindNotificationListResponse.java @@ -0,0 +1,26 @@ +package clap.server.adapter.inbound.web.dto.notification; + + +import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDateTime; + +public record FindNotificationListResponse( + @Schema(description = "알림 고유 ID", example = "1") + Long notificationId, + @Schema(description = "알림에 해당하는 작업 고유 ID", example = "1") + Long taskId, + @Schema(description = "알림 유형", example = "COMMENT or TASK_REQUESTED or STATUS_SWITCHED or " + + "PROCESSOR_ASSIGNED or PROCESSOR_CHANGED") + NotificationType notificationType, + @Schema(description = "알림 받는 회원 고유 ID", example = "1") + Long receiverId, + @Schema(description = "알림 제목", example = "VM 생성해주세요") + String taskTitle, + @Schema(description = "알림 내용", example = "진행 중 or 담당자 이름 등등") + String message, + @Schema(description = "알림 생성 시간", example = "2025-01-24 14:58") + LocalDateTime createdAt +) { +} diff --git a/src/main/java/clap/server/adapter/inbound/web/notification/FindNotificationController.java b/src/main/java/clap/server/adapter/inbound/web/notification/FindNotificationController.java new file mode 100644 index 00000000..0af8e4c3 --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/notification/FindNotificationController.java @@ -0,0 +1,44 @@ +package clap.server.adapter.inbound.web.notification; + +import clap.server.adapter.inbound.security.SecurityUserDetails; +import clap.server.adapter.inbound.web.dto.notification.FindNotificationListResponse; +import clap.server.application.port.inbound.notification.FindNotificationListUsecase; +import clap.server.common.annotation.architecture.WebAdapter; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "알림 관리 - 조회") +@WebAdapter +@RestController +@RequestMapping("/api/notifications") +@RequiredArgsConstructor +public class FindNotificationController { + + private final FindNotificationListUsecase findNotificationListUsecase; + + @Operation(summary = "알림 목록 조회 API") + @Parameters({ + @Parameter(name = "page", description = "조회할 목록 페이지 번호(0부터 시작)", example = "0", required = false), + @Parameter(name = "size", description = "조회할 목록 페이지 당 개수", example = "5", required = false) + }) + @GetMapping + public ResponseEntity> findNotificationList( + @AuthenticationPrincipal SecurityUserDetails securityUserDetails, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "5") int size) { + Pageable pageable = PageRequest.of(page, size); + return ResponseEntity.ok(findNotificationListUsecase.findNotificationList(securityUserDetails.getUserId(), pageable)); + } +} diff --git a/src/main/java/clap/server/adapter/outbound/infrastructure/redis/refresh/RefreshTokenEntity.java b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/refresh/RefreshTokenEntity.java index 2479d079..c71200ac 100644 --- a/src/main/java/clap/server/adapter/outbound/infrastructure/redis/refresh/RefreshTokenEntity.java +++ b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/refresh/RefreshTokenEntity.java @@ -1,5 +1,6 @@ package clap.server.adapter.outbound.infrastructure.redis.refresh; +import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; @@ -8,6 +9,7 @@ @Getter @RedisHash("refreshToken") +@Builder @ToString(of = {"memberId", "token", "ttl"}) @EqualsAndHashCode(of = {"memberId", "token"}) public class RefreshTokenEntity { diff --git a/src/main/java/clap/server/adapter/outbound/infrastructure/redis/refresh/RefreshTokenMapper.java b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/refresh/RefreshTokenMapper.java index 52115015..c9a95792 100644 --- a/src/main/java/clap/server/adapter/outbound/infrastructure/redis/refresh/RefreshTokenMapper.java +++ b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/refresh/RefreshTokenMapper.java @@ -1,12 +1,10 @@ package clap.server.adapter.outbound.infrastructure.redis.refresh; - -import clap.server.adapter.outbound.persistense.mapper.MemberPersistenceMapper; import clap.server.domain.model.auth.RefreshToken; import org.mapstruct.InheritInverseConfiguration; import org.mapstruct.Mapper; -@Mapper(componentModel = "spring", uses = {MemberPersistenceMapper.class}) +@Mapper(componentModel = "spring") public interface RefreshTokenMapper { @InheritInverseConfiguration RefreshToken toDomain(final RefreshTokenEntity entity); diff --git a/src/main/java/clap/server/adapter/outbound/persistense/NotificationPersistenceAdapter.java b/src/main/java/clap/server/adapter/outbound/persistense/NotificationPersistenceAdapter.java new file mode 100644 index 00000000..c2b0d0a1 --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/persistense/NotificationPersistenceAdapter.java @@ -0,0 +1,28 @@ +package clap.server.adapter.outbound.persistense; + +import clap.server.adapter.inbound.web.dto.notification.FindNotificationListResponse; +import clap.server.adapter.outbound.persistense.mapper.NotificationPersistenceMapper; +import clap.server.adapter.outbound.persistense.repository.notification.NotificationRepository; +import clap.server.application.mapper.NotificationMapper; +import clap.server.application.port.outbound.notification.LoadNotificationPort; +import clap.server.common.annotation.architecture.PersistenceAdapter; +import clap.server.domain.model.notification.Notification; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +@PersistenceAdapter +@RequiredArgsConstructor +public class NotificationPersistenceAdapter implements LoadNotificationPort { + + private final NotificationRepository notificationRepository; + private final NotificationPersistenceMapper notificationPersistenceMapper; + + + @Override + public Page findAllByReceiverId(Long receiverId, Pageable pageable) { + Page notificationList = notificationRepository.findAllByReceiver_MemberId(receiverId, pageable) + .map(notificationPersistenceMapper::toDomain); + return notificationList.map(NotificationMapper::toFindNoticeListResponse); + } +} diff --git a/src/main/java/clap/server/adapter/outbound/persistense/mapper/NotificationPersistenceMapper.java b/src/main/java/clap/server/adapter/outbound/persistense/mapper/NotificationPersistenceMapper.java new file mode 100644 index 00000000..2c2f06af --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/persistense/mapper/NotificationPersistenceMapper.java @@ -0,0 +1,18 @@ +package clap.server.adapter.outbound.persistense.mapper; + +import clap.server.adapter.outbound.persistense.entity.notification.NotificationEntity; +import clap.server.adapter.outbound.persistense.mapper.common.PersistenceMapper; +import clap.server.domain.model.notification.Notification; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring", uses = {TaskPersistenceMapper.class, MemberPersistenceMapper.class}) +public interface NotificationPersistenceMapper extends PersistenceMapper { + @Override + @Mapping(source = "read", target = "isRead") + Notification toDomain(final NotificationEntity entity); + + @Override + @Mapping(source = "read", target = "isRead") + NotificationEntity toEntity(final Notification notification); +} diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/notification/NotificationRepository.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/notification/NotificationRepository.java index eff9c6e3..68e99315 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/repository/notification/NotificationRepository.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/notification/NotificationRepository.java @@ -1,9 +1,14 @@ package clap.server.adapter.outbound.persistense.repository.notification; import clap.server.adapter.outbound.persistense.entity.notification.NotificationEntity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; + @Repository public interface NotificationRepository extends JpaRepository { + + Page findAllByReceiver_MemberId(Long receiverId, Pageable pageable); } \ No newline at end of file diff --git a/src/main/java/clap/server/application/mapper/NotificationMapper.java b/src/main/java/clap/server/application/mapper/NotificationMapper.java new file mode 100644 index 00000000..fb793d2c --- /dev/null +++ b/src/main/java/clap/server/application/mapper/NotificationMapper.java @@ -0,0 +1,20 @@ +package clap.server.application.mapper; + +import clap.server.adapter.inbound.web.dto.notification.FindNotificationListResponse; +import clap.server.domain.model.notification.Notification; + +public class NotificationMapper { + private NotificationMapper() {throw new IllegalArgumentException();} + + public static FindNotificationListResponse toFindNoticeListResponse(Notification notification) { + return new FindNotificationListResponse( + notification.getNotificationId(), + notification.getTask().getTaskId(), + notification.getType(), + notification.getReceiver().getMemberId(), + notification.getTask().getTitle(), + notification.getMessage() != null ? notification.getMessage() : null, + notification.getCreatedAt() + ); + } +} diff --git a/src/main/java/clap/server/application/mapper/response/AuthResponseMapper.java b/src/main/java/clap/server/application/mapper/response/AuthResponseMapper.java index f5545efa..3c9ddbdd 100644 --- a/src/main/java/clap/server/application/mapper/response/AuthResponseMapper.java +++ b/src/main/java/clap/server/application/mapper/response/AuthResponseMapper.java @@ -2,6 +2,8 @@ import clap.server.adapter.inbound.web.dto.auth.LoginResponse; import clap.server.adapter.inbound.web.dto.auth.MemberInfoResponse; +import clap.server.adapter.inbound.web.dto.auth.ReissueTokenResponse; +import clap.server.domain.model.auth.CustomJwts; import clap.server.domain.model.member.Member; public class AuthResponseMapper { @@ -27,4 +29,11 @@ public static MemberInfoResponse toMemberInfoResponse(Member member) { member.getStatus() ); } + + public static ReissueTokenResponse toReissueTokenResponse(final CustomJwts jwtTokens) { + return new ReissueTokenResponse( + jwtTokens.accessToken(), + jwtTokens.refreshToken() + ); + } } diff --git a/src/main/java/clap/server/application/port/inbound/auth/ReissueTokenUsecase.java b/src/main/java/clap/server/application/port/inbound/auth/ReissueTokenUsecase.java new file mode 100644 index 00000000..a9df09e2 --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/auth/ReissueTokenUsecase.java @@ -0,0 +1,7 @@ +package clap.server.application.port.inbound.auth; + +import clap.server.adapter.inbound.web.dto.auth.ReissueTokenResponse; + +public interface ReissueTokenUsecase { + ReissueTokenResponse reissueToken(String oldRefreshToken); +} diff --git a/src/main/java/clap/server/application/port/inbound/notification/FindNotificationListUsecase.java b/src/main/java/clap/server/application/port/inbound/notification/FindNotificationListUsecase.java new file mode 100644 index 00000000..26196d1b --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/notification/FindNotificationListUsecase.java @@ -0,0 +1,9 @@ +package clap.server.application.port.inbound.notification; + +import clap.server.adapter.inbound.web.dto.notification.FindNotificationListResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface FindNotificationListUsecase { + Page findNotificationList(Long receiverId, Pageable pageable); +} diff --git a/src/main/java/clap/server/application/port/outbound/notification/LoadNotificationPort.java b/src/main/java/clap/server/application/port/outbound/notification/LoadNotificationPort.java index 562e1c86..46602d90 100644 --- a/src/main/java/clap/server/application/port/outbound/notification/LoadNotificationPort.java +++ b/src/main/java/clap/server/application/port/outbound/notification/LoadNotificationPort.java @@ -1,8 +1,10 @@ package clap.server.application.port.outbound.notification; -import clap.server.domain.model.notification.Notification; -import java.util.Optional; +import clap.server.adapter.inbound.web.dto.notification.FindNotificationListResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + public interface LoadNotificationPort { - Optional findById(Long id); + Page findAllByReceiverId(Long receiverId, Pageable pageable); } \ No newline at end of file diff --git a/src/main/java/clap/server/application/service/auth/AuthService.java b/src/main/java/clap/server/application/service/auth/AuthService.java index ee960017..7015a047 100644 --- a/src/main/java/clap/server/application/service/auth/AuthService.java +++ b/src/main/java/clap/server/application/service/auth/AuthService.java @@ -4,6 +4,7 @@ import clap.server.adapter.outbound.persistense.entity.member.constant.MemberStatus; import clap.server.application.mapper.response.AuthResponseMapper; import clap.server.application.port.inbound.auth.AuthUsecase; +import clap.server.application.port.outbound.auth.CommandRefreshTokenPort; import clap.server.application.port.outbound.member.LoadMemberPort; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.domain.model.auth.CustomJwts; @@ -18,6 +19,7 @@ @RequiredArgsConstructor class AuthService implements AuthUsecase { private final LoadMemberPort loadMemberPort; + private final CommandRefreshTokenPort commandRefreshTokenPort; private final IssueTokenService issueTokenService; private final PasswordEncoder passwordEncoder; @@ -29,22 +31,25 @@ public LoginResponse login(String nickname, String password) { validatePassword(password, member.getPassword()); if (member.getStatus().equals(MemberStatus.APPROVAL_REQUEST)) { - String temporaryToken = issueTokenService.createTemporaryToken(member); + String temporaryToken = issueTokenService.issueTemporaryToken(member.getMemberId()); return AuthResponseMapper.toLoginResponse( temporaryToken, null, member ); } else { - CustomJwts jwtTokens = issueTokenService.createToken(member); + CustomJwts jwtTokens = issueTokenService.issueTokens(member); + commandRefreshTokenPort.save( + issueTokenService.issueRefreshToken(member.getMemberId()) + ); return AuthResponseMapper.toLoginResponse( jwtTokens.accessToken(), jwtTokens.refreshToken(), member ); } } - private void validatePassword(String inputPassword, String encodedPassword) { if (!passwordEncoder.matches(inputPassword, encodedPassword)) { throw new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND); } } + } diff --git a/src/main/java/clap/server/application/service/auth/IssueTokenService.java b/src/main/java/clap/server/application/service/auth/IssueTokenService.java index 42b9d348..2b8770dd 100644 --- a/src/main/java/clap/server/application/service/auth/IssueTokenService.java +++ b/src/main/java/clap/server/application/service/auth/IssueTokenService.java @@ -1,22 +1,21 @@ package clap.server.application.service.auth; +import clap.server.adapter.outbound.jwt.JwtClaims; import clap.server.adapter.outbound.jwt.access.AccessTokenClaim; import clap.server.adapter.outbound.jwt.access.temporary.TemporaryTokenClaim; import clap.server.adapter.outbound.jwt.refresh.RefreshTokenClaim; -import clap.server.application.port.outbound.auth.CommandRefreshTokenPort; +import clap.server.adapter.outbound.jwt.refresh.RefreshTokenClaimKeys; import clap.server.application.port.outbound.auth.JwtProvider; -import clap.server.application.port.outbound.auth.LoadRefreshTokenPort; import clap.server.domain.model.auth.CustomJwts; import clap.server.domain.model.auth.RefreshToken; import clap.server.domain.model.member.Member; -import clap.server.exception.AuthException; -import clap.server.exception.code.AuthErrorCode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.time.Duration; import java.time.LocalDateTime; +import java.util.function.Function; @RequiredArgsConstructor @Component @@ -25,56 +24,49 @@ class IssueTokenService { private final JwtProvider accessTokenProvider; private final JwtProvider refreshTokenProvider; private final JwtProvider temporaryTokenProvider; - private final LoadRefreshTokenPort loadRefreshTokenPort; - private final CommandRefreshTokenPort commandRefreshTokenPort; - public CustomJwts createToken(Member member) { + public CustomJwts issueTokens(Member member) { String accessToken = accessTokenProvider.createToken(AccessTokenClaim.of(member.getMemberId())); String refreshToken = refreshTokenProvider.createToken(RefreshTokenClaim.of(member.getMemberId())); - - commandRefreshTokenPort.save( - RefreshToken.of( - member.getMemberId(), refreshToken, - toSeconds(refreshTokenProvider.getExpiredDate(refreshToken)) - ) - ); - return CustomJwts.of(accessToken, refreshToken); } - public String createTemporaryToken(Member member) { - return temporaryTokenProvider.createToken(TemporaryTokenClaim.of(member.getMemberId())); + public String issueAccessToken(Long memberId) { + return accessTokenProvider.createToken(AccessTokenClaim.of(memberId)); } - private long toSeconds(LocalDateTime expiredDate) { - return Duration.between(LocalDateTime.now(), expiredDate).getSeconds(); - } - public RefreshToken refresh( - Long memberId, - String oldRefreshToken, - String newRefreshToken - ) throws IllegalArgumentException, IllegalStateException { - RefreshToken refreshToken = loadRefreshTokenPort.findByMemberId(memberId).orElseThrow( - ()-> new AuthException(AuthErrorCode.REFRESH_TOKEN_NOT_FOUND) + public RefreshToken issueRefreshToken(Long memberId) { + String refreshToken = refreshTokenProvider.createToken(RefreshTokenClaim.of(memberId)); + return RefreshToken.of( + memberId, refreshToken, + toSeconds(refreshTokenProvider.getExpiredDate(refreshToken)) ); - validateToken(oldRefreshToken, refreshToken); + } + + public String issueTemporaryToken(Long memberId) { + return temporaryTokenProvider.createToken(TemporaryTokenClaim.of(memberId)); + } - refreshToken.rotation(newRefreshToken); - commandRefreshTokenPort.save(refreshToken); - return refreshToken; + public Long resolveRefreshToken(String refreshToken) { + JwtClaims claims = refreshTokenProvider.parseJwtClaimsFromToken(refreshToken); + + return getClaimValue(claims, + RefreshTokenClaimKeys.USER_ID.getValue(), + Long::parseLong); } - private void validateToken(String oldRefreshToken, RefreshToken refreshToken) { - if (isTakenAway(oldRefreshToken, refreshToken.getToken())) { - commandRefreshTokenPort.delete(refreshToken); - throw new AuthException(AuthErrorCode.REFRESH_TOKEN_MISMATCHED); - } + private long toSeconds(LocalDateTime expiredDate) { + return Duration.between(LocalDateTime.now(), expiredDate).getSeconds(); } - private boolean isTakenAway(String requestRefreshToken, String expectedRefreshToken) { - return !requestRefreshToken.equals(expectedRefreshToken); + private static T getClaimValue(JwtClaims jwtClaims, String key, Function converter) { + Object value = jwtClaims.getClaims().get(key); + if (value != null) { + return converter.apply(value.toString()); + } + return null; } } diff --git a/src/main/java/clap/server/application/service/auth/ReissueTokenService.java b/src/main/java/clap/server/application/service/auth/ReissueTokenService.java new file mode 100644 index 00000000..022dd9e2 --- /dev/null +++ b/src/main/java/clap/server/application/service/auth/ReissueTokenService.java @@ -0,0 +1,68 @@ +package clap.server.application.service.auth; + +import clap.server.adapter.inbound.web.dto.auth.ReissueTokenResponse; +import clap.server.application.port.inbound.auth.ReissueTokenUsecase; +import clap.server.application.port.outbound.auth.CommandRefreshTokenPort; +import clap.server.application.port.outbound.auth.LoadRefreshTokenPort; +import clap.server.common.annotation.architecture.ApplicationService; +import clap.server.domain.model.auth.CustomJwts; +import clap.server.domain.model.auth.RefreshToken; +import clap.server.exception.AuthException; +import clap.server.exception.code.AuthErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +import static clap.server.application.mapper.response.AuthResponseMapper.toReissueTokenResponse; + +@ApplicationService +@RequiredArgsConstructor +class ReissueTokenService implements ReissueTokenUsecase { + private final IssueTokenService issueTokenService; + private final LoadRefreshTokenPort loadRefreshTokenPort; + private final CommandRefreshTokenPort commandRefreshTokenPort; + + @Transactional + public ReissueTokenResponse reissueToken(String oldRefreshToken) { + Long memberId = issueTokenService.resolveRefreshToken(oldRefreshToken); + RefreshToken newRefreshToken; + try { + newRefreshToken = refresh(memberId, oldRefreshToken, + issueTokenService.issueRefreshToken(memberId).getToken()); + } catch (IllegalArgumentException e) { + throw new AuthException(AuthErrorCode.EXPIRED_TOKEN); + } catch (IllegalStateException e) { + throw new AuthException(AuthErrorCode.TAKEN_AWAY_TOKEN); + } + + String newAccessToken = issueTokenService.issueAccessToken(memberId); + CustomJwts tokens = CustomJwts.of(newAccessToken, newRefreshToken.getToken()); + return toReissueTokenResponse(tokens); + } + + private RefreshToken refresh( + Long memberId, + String oldRefreshToken, + String newRefreshToken + ) throws IllegalArgumentException, IllegalStateException { + RefreshToken refreshToken = loadRefreshTokenPort.findByMemberId(memberId).orElseThrow( + () -> new AuthException(AuthErrorCode.REFRESH_TOKEN_NOT_FOUND) + ); + validateToken(oldRefreshToken, refreshToken); + + refreshToken.rotation(newRefreshToken); + commandRefreshTokenPort.save(refreshToken); + return refreshToken; + } + + private void validateToken(String oldRefreshToken, RefreshToken refreshToken) { + if (isTakenAway(oldRefreshToken, refreshToken.getToken())) { + commandRefreshTokenPort.delete(refreshToken); + throw new AuthException(AuthErrorCode.REFRESH_TOKEN_MISMATCHED); + } + } + + private boolean isTakenAway(String requestRefreshToken, String expectedRefreshToken) { + return !requestRefreshToken.equals(expectedRefreshToken); + } + +} diff --git a/src/main/java/clap/server/application/service/notification/FindNotificationListService.java b/src/main/java/clap/server/application/service/notification/FindNotificationListService.java new file mode 100644 index 00000000..85902515 --- /dev/null +++ b/src/main/java/clap/server/application/service/notification/FindNotificationListService.java @@ -0,0 +1,24 @@ +package clap.server.application.service.notification; + +import clap.server.adapter.inbound.web.dto.notification.FindNotificationListResponse; +import clap.server.application.port.inbound.notification.FindNotificationListUsecase; +import clap.server.application.port.outbound.notification.LoadNotificationPort; +import clap.server.common.annotation.architecture.ApplicationService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.transaction.annotation.Transactional; + +@ApplicationService +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class FindNotificationListService implements FindNotificationListUsecase { + + private final LoadNotificationPort loadNotificationPort; + + + @Override + public Page findNotificationList(Long receiverId, Pageable pageable) { + return loadNotificationPort.findAllByReceiverId(receiverId, pageable); + } +} diff --git a/src/test/java/clap/server/notification/NotificationServiceTest.java b/src/test/java/clap/server/notification/NotificationServiceTest.java new file mode 100644 index 00000000..dec1f21e --- /dev/null +++ b/src/test/java/clap/server/notification/NotificationServiceTest.java @@ -0,0 +1,67 @@ +package clap.server.notification; + +import clap.server.adapter.inbound.web.dto.notification.FindNotificationListResponse; +import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType; +import clap.server.application.port.outbound.notification.LoadNotificationPort; +import clap.server.application.service.notification.FindNotificationListService; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class NotificationServiceTest { + + @Mock + private LoadNotificationPort loadNotificationPort; + + @InjectMocks + private FindNotificationListService findNotificationListService; + + + @Test + public void testFindNotificationList() { + //Given + // 목록 조회 테스트이므로 여러개의 데이터를 List에 저장 + FindNotificationListResponse findNotificationListResponse = new FindNotificationListResponse( + 1L, 1L, NotificationType.PROCESSOR_ASSIGNED, 1L, "VM 생성해주세요", "이규동", LocalDateTime.now() + ); + FindNotificationListResponse findNotificationListResponse2 = new FindNotificationListResponse( + 1L, 1L, NotificationType.PROCESSOR_CHANGED, 1L, "VM 생성해주세요", "이규동", LocalDateTime.now() + ); + FindNotificationListResponse findNotificationListResponse3 = new FindNotificationListResponse( + 1L, 1L, NotificationType.STATUS_SWITCHED, 1L, "VM 생성해주세요", "진행중", LocalDateTime.now() + ); + + List notificationList = List.of( + findNotificationListResponse, findNotificationListResponse2, findNotificationListResponse3 + ); + + Page page = new PageImpl<>(notificationList); + Pageable pageable = Pageable.ofSize(3); + + //Mock + when(loadNotificationPort.findAllByReceiverId(1L, pageable)).thenReturn(page); + + //When + Page result = findNotificationListService.findNotificationList(1L, pageable); + + //Then + Assertions.assertEquals(3, result.getContent().size()); + + Assertions.assertEquals("VM 생성해주세요", result.getContent().get(0).taskTitle()); + Assertions.assertEquals("VM 생성해주세요", result.getContent().get(1).taskTitle()); + Assertions.assertEquals("VM 생성해주세요", result.getContent().get(2).taskTitle()); + } +}