Skip to content
Original file line number Diff line number Diff line change
@@ -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<ReissueTokenResponse> login(@RequestHeader String refreshToken) {
return ResponseEntity.ok(reissueTokenUsecase.reissueToken(refreshToken));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package clap.server.adapter.inbound.web.dto.auth;

public record ReissueTokenResponse(
String accessToken,
String refreshToken
) {
}
Original file line number Diff line number Diff line change
@@ -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
) {
}
Original file line number Diff line number Diff line change
@@ -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<Page<FindNotificationListResponse>> 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));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package clap.server.adapter.outbound.infrastructure.redis.refresh;

import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
Expand All @@ -8,6 +9,7 @@

@Getter
@RedisHash("refreshToken")
@Builder
@ToString(of = {"memberId", "token", "ttl"})
@EqualsAndHashCode(of = {"memberId", "token"})
public class RefreshTokenEntity {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<FindNotificationListResponse> findAllByReceiverId(Long receiverId, Pageable pageable) {
Page<Notification> notificationList = notificationRepository.findAllByReceiver_MemberId(receiverId, pageable)
.map(notificationPersistenceMapper::toDomain);
return notificationList.map(NotificationMapper::toFindNoticeListResponse);
}
}
Original file line number Diff line number Diff line change
@@ -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<NotificationEntity, Notification> {
@Override
@Mapping(source = "read", target = "isRead")
Notification toDomain(final NotificationEntity entity);

@Override
@Mapping(source = "read", target = "isRead")
NotificationEntity toEntity(final Notification notification);
}
Original file line number Diff line number Diff line change
@@ -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<NotificationEntity, Long> {

Page<NotificationEntity> findAllByReceiver_MemberId(Long receiverId, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -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()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()
);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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<FindNotificationListResponse> findNotificationList(Long receiverId, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -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<Notification> findById(Long id);
Page<FindNotificationListResponse> findAllByReceiverId(Long receiverId, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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);
}
}

}
Loading
Loading