From e1e9d3b0cf98d49d026708be32fd36912888d295 Mon Sep 17 00:00:00 2001 From: vayaconchoi Date: Fri, 1 Aug 2025 19:12:54 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(=ED=8C=8C=EC=9D=BC=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=EA=B6=8C=ED=95=9C=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80):=20=ED=8C=8C=EC=9D=BC=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=EA=B6=8C=ED=95=9C=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20DP-184?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 파일 시스템 권한 관련 메시지 - 오너나 멤버가 아니면 '오너 및 멤버만 접근할 수 있습니다' - 레포지토리가 없으면 '존재하지 않는 레포지토리 입니다' --- .../file/service/FileService.java | 58 ++++++++++++++++--- .../global/exception/ErrorCode.java | 1 + .../history/service/HistoryService.java | 27 +++++++-- 3 files changed, 74 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/deepdirect/deepwebide_be/file/service/FileService.java b/src/main/java/com/deepdirect/deepwebide_be/file/service/FileService.java index dc994a4..860d8cf 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/file/service/FileService.java +++ b/src/main/java/com/deepdirect/deepwebide_be/file/service/FileService.java @@ -30,9 +30,14 @@ public class FileService { public List getFileTree(Long repositoryId, Long userId) { // 1. 레포지토리/권한 체크 - repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) + repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId) .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); + boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent(); + if (!hasAccess) { + throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED); + } + // 2. 모든 FileNode 조회 (1쿼리) List allNodes = fileNodeRepository.findAllByRepositoryId(repositoryId); @@ -75,9 +80,14 @@ public FileNodeResponse createFileOrFolder(Long repositoryId, Long userId, FileC } // 1. 레포 권한 체크 - Repository repo = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) + Repository repo = repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId) .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); + boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent(); + if (!hasAccess) { + throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED); + } + // 2. 부모 폴더 체크 (없으면 null) FileNode parent = null; String parentPath = ""; @@ -130,8 +140,14 @@ public FileNodeResponse createFileOrFolder(Long repositoryId, Long userId, FileC @Transactional public FileRenameResponse renameFileOrFolder(Long repositoryId, Long fileId, Long userId, String newFileName) { // 1. 권한/레포 체크 - repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) + repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId) .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); + + boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent(); + if (!hasAccess) { + throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED); + } + FileNode fileNode = findFileNodeWithRepositoryCheck(repositoryId, fileId); if (fileNode.getFileType() == FileType.FILE) { @@ -170,8 +186,14 @@ private void updateChildPathsRecursively(FileNode parentNode) { @Transactional public void deleteFileOrFolder(Long repositoryId, Long fileId, Long userId) { - repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) + repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId) .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); + + boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent(); + if (!hasAccess) { + throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED); + } + FileNode node = findFileNodeWithRepositoryCheck(repositoryId, fileId); // (폴더일 경우) 하위 전체 삭제 (재귀) @@ -210,8 +232,13 @@ public FileNodeResponse moveFileOrFolder( throw new GlobalException(ErrorCode.PARENT_ID_REQUIRED); } - repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) + repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId) .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); + + boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent(); + if (!hasAccess) { + throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED); + } FileNode fileNode = findFileNodeWithRepositoryCheck(repositoryId, fileId); // 새 부모 폴더 체크 @@ -261,8 +288,13 @@ private boolean isDescendant(FileNode node, FileNode targetParent) { @Transactional(readOnly = true) public FileContentResponse getFileContent(Long repositoryId, Long fileId, Long userId) { - repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) + repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId) .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); + + boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent(); + if (!hasAccess) { + throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED); + } FileNode fileNode = findFileNodeWithRepositoryCheck(repositoryId, fileId); if (fileNode.getFileType() == FileType.FOLDER) { @@ -297,9 +329,14 @@ public FileContentResponse getFileContent(Long repositoryId, Long fileId, Long u @Transactional public FileContentSaveResponse saveFileContent(Long repositoryId, Long fileId, Long userId, String content) { // 1. 레포 권한 및 존재 확인 - Repository repo = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) + repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId) .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); + boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent(); + if (!hasAccess) { + throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED); + } + // 2. 파일 노드 + 소속 레포 검증 FileNode fileNode = findFileNodeWithRepositoryCheck(repositoryId, fileId); @@ -343,9 +380,14 @@ public FileNodeResponse uploadFile(Long repositoryId, Long userId, Long parentId // 1. 권한/레포 체크 - Repository repo = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) + Repository repo = repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId) .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); + boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent(); + if (!hasAccess) { + throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED); + } + // 2. 부모 폴더 체크 FileNode parent = null; String parentPath = ""; diff --git a/src/main/java/com/deepdirect/deepwebide_be/global/exception/ErrorCode.java b/src/main/java/com/deepdirect/deepwebide_be/global/exception/ErrorCode.java index 226c4c9..3c0a531 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/global/exception/ErrorCode.java +++ b/src/main/java/com/deepdirect/deepwebide_be/global/exception/ErrorCode.java @@ -63,6 +63,7 @@ public enum ErrorCode { ENTRY_CODE_ACCESS_DENIED(HttpStatus.FORBIDDEN, "오너만 확인할 수 있습니다."), ENTRY_CODE_REISSUE_DENIED(HttpStatus.FORBIDDEN, "해당 레포지토리의 소유자만 입장 코드를 재발급할 수 있습니다."), NOT_OWNER_TO_KICK(HttpStatus.FORBIDDEN,"해당 레포의 소유자만 멤버를 강퇴할 수 있습니다."), + REPOSITORY_ACCESS_DENIED(HttpStatus.FORBIDDEN, "오너 및 멤버만 접근할 수 있습니다."), // 404 NOT FOUND USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."), diff --git a/src/main/java/com/deepdirect/deepwebide_be/history/service/HistoryService.java b/src/main/java/com/deepdirect/deepwebide_be/history/service/HistoryService.java index 6eb7b4a..2903ada 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/history/service/HistoryService.java +++ b/src/main/java/com/deepdirect/deepwebide_be/history/service/HistoryService.java @@ -46,9 +46,13 @@ public class HistoryService { @Transactional public HistorySaveResponse saveHistory(Long repositoryId, Long userId, HistorySaveRequest request) { // 1. 권한 체크 및 레포 조회 - Repository repo = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) + Repository repo = repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId) .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); + boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent(); + if (!hasAccess) { + throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED); + } // 2. 현재 DB의 파일/폴더 전체 조회 List dbNodes = fileNodeRepository.findAllByRepositoryId(repositoryId); @@ -97,9 +101,14 @@ public HistorySaveResponse saveHistory(Long repositoryId, Long userId, HistorySa @Transactional(readOnly = true) public HistoryDetailResponse getHistoryDetail(Long repositoryId, Long historyId, Long userId) { // 1. 권한 체크 & 레포 확인 - Repository repo = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) + Repository repo = repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId) .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); + boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent(); + if (!hasAccess) { + throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED); + } + // 2. 히스토리 조회 History history = historyRepository.findById(historyId) .orElseThrow(() -> new GlobalException(ErrorCode.HISTORY_NOT_FOUND)); @@ -130,9 +139,14 @@ public HistoryDetailResponse getHistoryDetail(Long repositoryId, Long historyId, @Transactional(readOnly = true) public List getHistories(Long repositoryId, Long userId) { // 1. 권한 체크 & 레포 확인 - Repository repo = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) + Repository repo = repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId) .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); + boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent(); + if (!hasAccess) { + throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED); + } + // 2. 히스토리 목록(최신순) 조회 List histories = historyRepository.findByRepositoryOrderByCreatedAtDesc(repo); @@ -157,9 +171,14 @@ public List getHistories(Long repositoryId, Long userId) { @Transactional public HistoryRestoreResponse restoreHistory(Long repositoryId, Long historyId, Long userId) { // 1. 레포/오너 확인 - Repository repo = repositoryRepository.findById(repositoryId) + Repository repo = repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId) .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); + boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent(); + if (!hasAccess) { + throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED); + } + if (!repo.getOwner().getId().equals(userId)) { throw new GlobalException(ErrorCode.FORBIDDEN); } From b90e9bfe7caed975e6a3fc67d925a26c3c4d007a Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Fri, 1 Aug 2025 19:58:41 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat(Chat):=20=EC=B1=84=ED=8C=85=EC=9D=98?= =?UTF-8?q?=20=EC=A0=95=EC=83=81=ED=99=94=EB=A5=BC=20=ED=96=A5=ED=95=B4?= =?UTF-8?q?=EC=84=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ChatWebSocketController.java | 9 ++++ .../dto/response/ChatMessageBroadcast.java | 4 +- .../chat/util/RedisSubscriber.java | 45 +++++++++++-------- .../deepwebide_be/chat/util/StompHandler.java | 7 +++ .../global/config/WebSocketConfig.java | 2 +- 5 files changed, 46 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/deepdirect/deepwebide_be/chat/controller/ChatWebSocketController.java b/src/main/java/com/deepdirect/deepwebide_be/chat/controller/ChatWebSocketController.java index 852fe64..83ede51 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/chat/controller/ChatWebSocketController.java +++ b/src/main/java/com/deepdirect/deepwebide_be/chat/controller/ChatWebSocketController.java @@ -4,17 +4,22 @@ import com.deepdirect.deepwebide_be.chat.dto.response.ChatMessageBroadcast; import com.deepdirect.deepwebide_be.chat.service.ChatMessageWriteService; import com.deepdirect.deepwebide_be.chat.util.RedisPublisher; +import com.deepdirect.deepwebide_be.member.repository.UserRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.stereotype.Controller; + +@Slf4j @Controller @RequiredArgsConstructor public class ChatWebSocketController { private final ChatMessageWriteService chatMessageWriteService; private final RedisPublisher redisPublisher; + private final UserRepository userRepository; @MessageMapping("/repositories/{repositoryId}/chat/send") public void sendMessage( @@ -25,6 +30,10 @@ public void sendMessage( // 1. WebSocket 세션에서 userId 추출 Long userId = (Long) headerAccessor.getSessionAttributes().get("userId"); + String username = userRepository.findById(userId).get().getUsername(); + log.info("WebSocket 메시지 전송: userId={}, username={}", userId, username); + + System.out.println(request.getMessage()); // 2. 메시지 저장 + DTO 응답 변환 ChatMessageBroadcast broadcast = chatMessageWriteService.saveChatMessage(userId, request); diff --git a/src/main/java/com/deepdirect/deepwebide_be/chat/dto/response/ChatMessageBroadcast.java b/src/main/java/com/deepdirect/deepwebide_be/chat/dto/response/ChatMessageBroadcast.java index 05049e0..3c69221 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/chat/dto/response/ChatMessageBroadcast.java +++ b/src/main/java/com/deepdirect/deepwebide_be/chat/dto/response/ChatMessageBroadcast.java @@ -4,8 +4,8 @@ import com.deepdirect.deepwebide_be.chat.domain.ChatMessageType; import com.deepdirect.deepwebide_be.member.domain.User; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Builder; -import lombok.Getter; +import lombok.*; +import org.springframework.stereotype.Service; import java.time.LocalDateTime; diff --git a/src/main/java/com/deepdirect/deepwebide_be/chat/util/RedisSubscriber.java b/src/main/java/com/deepdirect/deepwebide_be/chat/util/RedisSubscriber.java index fc113a5..a8e91c1 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/chat/util/RedisSubscriber.java +++ b/src/main/java/com/deepdirect/deepwebide_be/chat/util/RedisSubscriber.java @@ -1,6 +1,8 @@ package com.deepdirect.deepwebide_be.chat.util; import com.deepdirect.deepwebide_be.chat.dto.response.ChatMessageBroadcast; +import com.deepdirect.deepwebide_be.chat.dto.response.ChatSystemMessageResponse; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import io.sentry.Sentry; import lombok.RequiredArgsConstructor; @@ -24,28 +26,35 @@ public class RedisSubscriber implements MessageListener { public void onMessage(Message message, byte[] pattern) { try { String raw = new String(message.getBody(), StandardCharsets.UTF_8); - ChatMessageBroadcast broadcast = objectMapper.readValue(raw, ChatMessageBroadcast.class); - - // isMine은 false로 변경 (다른 사람들에게 보내는 메시지) - ChatMessageBroadcast response = ChatMessageBroadcast.builder() - .type(broadcast.getType()) - .messageId(broadcast.getMessageId()) - .senderId(broadcast.getSenderId()) - .senderNickname(broadcast.getSenderNickname()) - .senderProfileImageUrl(broadcast.getSenderProfileImageUrl()) - .message(broadcast.getMessage()) - .sentAt(broadcast.getSentAt()) - .isMine(false) - .build(); + JsonNode root = objectMapper.readTree(raw); + String type = root.get("type").asText(); String topic = new String(message.getChannel(), StandardCharsets.UTF_8); Long repositoryId = Long.parseLong(topic.split(":")[1]); - // 구독 중인 사용자들에게 메시지 전송 - messagingTemplate.convertAndSend( - "/sub/repositories/" + repositoryId + "/chat", - response - ); + log.info("📩 Redis 메시지 도착! raw: {}", raw); + + if ("CHAT".equals(type)) { + ChatMessageBroadcast broadcast = objectMapper.treeToValue(root, ChatMessageBroadcast.class); + ChatMessageBroadcast response = ChatMessageBroadcast.builder() + .type(broadcast.getType()) + .messageId(broadcast.getMessageId()) + .senderId(broadcast.getSenderId()) + .senderNickname(broadcast.getSenderNickname()) + .senderProfileImageUrl(broadcast.getSenderProfileImageUrl()) + .message(broadcast.getMessage()) + .codeReference(broadcast.getCodeReference()) + .sentAt(broadcast.getSentAt()) + .isMine(false) + .build(); + + messagingTemplate.convertAndSend("/sub/repositories/" + repositoryId + "/chat", response); + + } else { + // USER_JOINED, USER_LEFT + ChatSystemMessageResponse system = objectMapper.treeToValue(root, ChatSystemMessageResponse.class); + messagingTemplate.convertAndSend("/topic/repositories/" + repositoryId + "/chat", system); + } } catch (Exception e) { log.error("❌ RedisSubscriber: 메시지 처리 중 에러 발생", e); } diff --git a/src/main/java/com/deepdirect/deepwebide_be/chat/util/StompHandler.java b/src/main/java/com/deepdirect/deepwebide_be/chat/util/StompHandler.java index f777ae6..9a079ce 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/chat/util/StompHandler.java +++ b/src/main/java/com/deepdirect/deepwebide_be/chat/util/StompHandler.java @@ -38,6 +38,13 @@ public Message preSend(Message message, MessageChannel channel) { User user = userRepository.findById(userId) .orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND)); + + String username = user.getUsername(); + String nickname = user.getNickname(); + log.info("WebSocket 연결: userId={}", userId); + log.info("WebSocket 연결: username={}", username); + log.info("WebSocket 연결: nickname={}", nickname); + accessor.setUser(new CustomUserDetails(user)); } diff --git a/src/main/java/com/deepdirect/deepwebide_be/global/config/WebSocketConfig.java b/src/main/java/com/deepdirect/deepwebide_be/global/config/WebSocketConfig.java index b1e18c0..aa1cf2a 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/global/config/WebSocketConfig.java +++ b/src/main/java/com/deepdirect/deepwebide_be/global/config/WebSocketConfig.java @@ -34,7 +34,7 @@ public void registerStompEndpoints(StompEndpointRegistry registry) { @Override public void configureMessageBroker(MessageBrokerRegistry registry) { -// registry.enableSimpleBroker("/topic"); // 구독 경로 (브라우저가 받을 때) + registry.enableSimpleBroker("/sub", "/topic"); // 구독 경로 (브라우저가 받을 때) registry.setApplicationDestinationPrefixes("/app"); // 발행 경로 (브라우저가 보낼 때) }