Allow public GET access to services and categories

This commit is contained in:
2026-03-08 15:17:49 -06:00
parent 992e610e4b
commit 68087c8f82
17 changed files with 699 additions and 4 deletions

View File

@@ -0,0 +1,25 @@
package com.petshop.backend.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws/chat")
.setAllowedOriginPatterns("*")
.withSockJS();
}
}

View File

@@ -14,7 +14,6 @@ import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/adoptions")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public class AdoptionController {
private final AdoptionService adoptionService;
@@ -24,6 +23,7 @@ public class AdoptionController {
}
@GetMapping
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<Page<AdoptionResponse>> getAllAdoptions(
@RequestParam(required = false) String q,
Pageable pageable) {
@@ -31,16 +31,19 @@ public class AdoptionController {
}
@GetMapping("/{id}")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<AdoptionResponse> getAdoptionById(@PathVariable Long id) {
return ResponseEntity.ok(adoptionService.getAdoptionById(id));
}
@PostMapping
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<AdoptionResponse> createAdoption(@Valid @RequestBody AdoptionRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(adoptionService.createAdoption(request));
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<AdoptionResponse> updateAdoption(
@PathVariable Long id,
@Valid @RequestBody AdoptionRequest request) {
@@ -48,12 +51,14 @@ public class AdoptionController {
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<Void> deleteAdoption(@PathVariable Long id) {
adoptionService.deleteAdoption(id);
return ResponseEntity.noContent().build();
}
@DeleteMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> bulkDeleteAdoptions(@Valid @RequestBody BulkDeleteRequest request) {
adoptionService.bulkDeleteAdoptions(request);
return ResponseEntity.noContent().build();

View File

@@ -17,7 +17,6 @@ import java.util.List;
@RestController
@RequestMapping("/api/v1/appointments")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public class AppointmentController {
private final AppointmentService appointmentService;
@@ -27,6 +26,7 @@ public class AppointmentController {
}
@GetMapping
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<Page<AppointmentResponse>> getAllAppointments(
@RequestParam(required = false) String q,
Pageable pageable) {
@@ -34,16 +34,19 @@ public class AppointmentController {
}
@GetMapping("/{id}")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<AppointmentResponse> getAppointmentById(@PathVariable Long id) {
return ResponseEntity.ok(appointmentService.getAppointmentById(id));
}
@PostMapping
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<AppointmentResponse> createAppointment(@Valid @RequestBody AppointmentRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(appointmentService.createAppointment(request));
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<AppointmentResponse> updateAppointment(
@PathVariable Long id,
@Valid @RequestBody AppointmentRequest request) {
@@ -51,12 +54,14 @@ public class AppointmentController {
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<Void> deleteAppointment(@PathVariable Long id) {
appointmentService.deleteAppointment(id);
return ResponseEntity.noContent().build();
}
@DeleteMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> bulkDeleteAppointments(@Valid @RequestBody BulkDeleteRequest request) {
appointmentService.bulkDeleteAppointments(request);
return ResponseEntity.noContent().build();

View File

@@ -14,7 +14,6 @@ import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/categories")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public class CategoryController {
private final CategoryService categoryService;
@@ -36,11 +35,13 @@ public class CategoryController {
}
@PostMapping
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<CategoryResponse> createCategory(@Valid @RequestBody CategoryRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(categoryService.createCategory(request));
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<CategoryResponse> updateCategory(
@PathVariable Long id,
@Valid @RequestBody CategoryRequest request) {
@@ -48,12 +49,14 @@ public class CategoryController {
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<Void> deleteCategory(@PathVariable Long id) {
categoryService.deleteCategory(id);
return ResponseEntity.noContent().build();
}
@DeleteMapping
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<Void> bulkDeleteCategories(@Valid @RequestBody BulkDeleteRequest request) {
categoryService.bulkDeleteCategories(request);
return ResponseEntity.noContent().build();

View File

@@ -0,0 +1,80 @@
package com.petshop.backend.controller;
import com.petshop.backend.dto.chat.ConversationRequest;
import com.petshop.backend.dto.chat.ConversationResponse;
import com.petshop.backend.dto.chat.MessageRequest;
import com.petshop.backend.dto.chat.MessageResponse;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.service.ChatService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/chat")
public class ChatController {
private final ChatService chatService;
private final UserRepository userRepository;
public ChatController(ChatService chatService, UserRepository userRepository) {
this.chatService = chatService;
this.userRepository = userRepository;
}
private User getCurrentUser() {
UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return userRepository.findByUsername(userDetails.getUsername())
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
@PostMapping("/conversations")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<ConversationResponse> createConversation(@Valid @RequestBody ConversationRequest request) {
User user = getCurrentUser();
ConversationResponse response = chatService.createConversation(user.getId(), request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
@GetMapping("/conversations")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<List<ConversationResponse>> getConversations() {
User user = getCurrentUser();
List<ConversationResponse> conversations = chatService.getConversations(user.getId(), user.getRole());
return ResponseEntity.ok(conversations);
}
@GetMapping("/conversations/{id}")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<ConversationResponse> getConversation(@PathVariable Long id) {
User user = getCurrentUser();
ConversationResponse conversation = chatService.getConversation(id, user.getId(), user.getRole());
return ResponseEntity.ok(conversation);
}
@PostMapping("/conversations/{id}/messages")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<MessageResponse> sendMessage(
@PathVariable Long id,
@Valid @RequestBody MessageRequest request) {
User user = getCurrentUser();
MessageResponse message = chatService.sendMessage(id, user.getId(), request);
return ResponseEntity.status(HttpStatus.CREATED).body(message);
}
@GetMapping("/conversations/{id}/messages")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<List<MessageResponse>> getMessages(@PathVariable Long id) {
User user = getCurrentUser();
List<MessageResponse> messages = chatService.getMessages(id, user.getId(), user.getRole());
return ResponseEntity.ok(messages);
}
}

View File

@@ -14,7 +14,6 @@ import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/services")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public class ServiceController {
private final ServiceService serviceService;
@@ -36,11 +35,13 @@ public class ServiceController {
}
@PostMapping
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<ServiceResponse> createService(@Valid @RequestBody ServiceRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(serviceService.createService(request));
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<ServiceResponse> updateService(
@PathVariable Long id,
@Valid @RequestBody ServiceRequest request) {
@@ -48,12 +49,14 @@ public class ServiceController {
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<Void> deleteService(@PathVariable Long id) {
serviceService.deleteService(id);
return ResponseEntity.noContent().build();
}
@DeleteMapping
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<Void> bulkDeleteServices(@Valid @RequestBody BulkDeleteRequest request) {
serviceService.bulkDeleteServices(request);
return ResponseEntity.noContent().build();

View File

@@ -0,0 +1,23 @@
package com.petshop.backend.dto.chat;
import jakarta.validation.constraints.NotBlank;
public class ConversationRequest {
@NotBlank(message = "Initial message is required")
private String message;
public ConversationRequest() {
}
public ConversationRequest(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

View File

@@ -0,0 +1,96 @@
package com.petshop.backend.dto.chat;
import com.petshop.backend.entity.Conversation;
import java.time.LocalDateTime;
public class ConversationResponse {
private Long id;
private Long customerId;
private Long staffId;
private String status;
private String lastMessage;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public ConversationResponse() {
}
public ConversationResponse(Long id, Long customerId, Long staffId, String status, String lastMessage, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.id = id;
this.customerId = customerId;
this.staffId = staffId;
this.status = status;
this.lastMessage = lastMessage;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
public static ConversationResponse fromEntity(Conversation conversation, String lastMessage) {
ConversationResponse response = new ConversationResponse();
response.setId(conversation.getId());
response.setCustomerId(conversation.getCustomerId());
response.setStaffId(conversation.getStaffId());
response.setStatus(conversation.getStatus().name());
response.setLastMessage(lastMessage);
response.setCreatedAt(conversation.getCreatedAt());
response.setUpdatedAt(conversation.getUpdatedAt());
return response;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public Long getStaffId() {
return staffId;
}
public void setStaffId(Long staffId) {
this.staffId = staffId;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getLastMessage() {
return lastMessage;
}
public void setLastMessage(String lastMessage) {
this.lastMessage = lastMessage;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -0,0 +1,23 @@
package com.petshop.backend.dto.chat;
import jakarta.validation.constraints.NotBlank;
public class MessageRequest {
@NotBlank(message = "Message content is required")
private String content;
public MessageRequest() {
}
public MessageRequest(String content) {
this.content = content;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}

View File

@@ -0,0 +1,85 @@
package com.petshop.backend.dto.chat;
import com.petshop.backend.entity.Message;
import java.time.LocalDateTime;
public class MessageResponse {
private Long id;
private Long conversationId;
private Long senderId;
private String content;
private LocalDateTime timestamp;
private Boolean isRead;
public MessageResponse() {
}
public MessageResponse(Long id, Long conversationId, Long senderId, String content, LocalDateTime timestamp, Boolean isRead) {
this.id = id;
this.conversationId = conversationId;
this.senderId = senderId;
this.content = content;
this.timestamp = timestamp;
this.isRead = isRead;
}
public static MessageResponse fromEntity(Message message) {
MessageResponse response = new MessageResponse();
response.setId(message.getId());
response.setConversationId(message.getConversationId());
response.setSenderId(message.getSenderId());
response.setContent(message.getContent());
response.setTimestamp(message.getTimestamp());
response.setIsRead(message.getIsRead());
return response;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getConversationId() {
return conversationId;
}
public void setConversationId(Long conversationId) {
this.conversationId = conversationId;
}
public Long getSenderId() {
return senderId;
}
public void setSenderId(Long senderId) {
this.senderId = senderId;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
public void setTimestamp(LocalDateTime timestamp) {
this.timestamp = timestamp;
}
public Boolean getIsRead() {
return isRead;
}
public void setIsRead(Boolean isRead) {
this.isRead = isRead;
}
}

View File

@@ -0,0 +1,98 @@
package com.petshop.backend.entity;
import jakarta.persistence.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
@Entity
@Table(name = "conversation")
public class Conversation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private Long customerId;
@Column
private Long staffId;
@Enumerated(EnumType.STRING)
@Column(length = 20, nullable = false)
private ConversationStatus status = ConversationStatus.OPEN;
@CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
public enum ConversationStatus {
OPEN, CLOSED
}
public Conversation() {
}
public Conversation(Long id, Long customerId, Long staffId, ConversationStatus status, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.id = id;
this.customerId = customerId;
this.staffId = staffId;
this.status = status;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public Long getStaffId() {
return staffId;
}
public void setStaffId(Long staffId) {
this.staffId = staffId;
}
public ConversationStatus getStatus() {
return status;
}
public void setStatus(ConversationStatus status) {
this.status = status;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -0,0 +1,91 @@
package com.petshop.backend.entity;
import jakarta.persistence.*;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
@Entity
@Table(name = "message")
public class Message {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private Long conversationId;
@Column(nullable = false)
private Long senderId;
@Column(nullable = false, columnDefinition = "TEXT")
private String content;
@CreationTimestamp
@Column(nullable = false, updatable = false)
private LocalDateTime timestamp;
@Column(nullable = false)
private Boolean isRead = false;
public Message() {
}
public Message(Long id, Long conversationId, Long senderId, String content, LocalDateTime timestamp, Boolean isRead) {
this.id = id;
this.conversationId = conversationId;
this.senderId = senderId;
this.content = content;
this.timestamp = timestamp;
this.isRead = isRead;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getConversationId() {
return conversationId;
}
public void setConversationId(Long conversationId) {
this.conversationId = conversationId;
}
public Long getSenderId() {
return senderId;
}
public void setSenderId(Long senderId) {
this.senderId = senderId;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
public void setTimestamp(LocalDateTime timestamp) {
this.timestamp = timestamp;
}
public Boolean getIsRead() {
return isRead;
}
public void setIsRead(Boolean isRead) {
this.isRead = isRead;
}
}

View File

@@ -0,0 +1,13 @@
package com.petshop.backend.repository;
import com.petshop.backend.entity.Conversation;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ConversationRepository extends JpaRepository<Conversation, Long> {
List<Conversation> findByCustomerId(Long customerId);
List<Conversation> findByStaffId(Long staffId);
}

View File

@@ -0,0 +1,12 @@
package com.petshop.backend.repository;
import com.petshop.backend.entity.Message;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface MessageRepository extends JpaRepository<Message, Long> {
List<Message> findByConversationIdOrderByTimestampAsc(Long conversationId);
}

View File

@@ -0,0 +1,126 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.chat.ConversationRequest;
import com.petshop.backend.dto.chat.ConversationResponse;
import com.petshop.backend.dto.chat.MessageRequest;
import com.petshop.backend.dto.chat.MessageResponse;
import com.petshop.backend.entity.Conversation;
import com.petshop.backend.entity.Message;
import com.petshop.backend.entity.User;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.ConversationRepository;
import com.petshop.backend.repository.MessageRepository;
import com.petshop.backend.repository.UserRepository;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class ChatService {
private final ConversationRepository conversationRepository;
private final MessageRepository messageRepository;
private final UserRepository userRepository;
public ChatService(ConversationRepository conversationRepository,
MessageRepository messageRepository,
UserRepository userRepository) {
this.conversationRepository = conversationRepository;
this.messageRepository = messageRepository;
this.userRepository = userRepository;
}
@Transactional
public ConversationResponse createConversation(Long userId, ConversationRequest request) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
Conversation conversation = new Conversation();
conversation.setCustomerId(userId);
conversation.setStatus(Conversation.ConversationStatus.OPEN);
conversation = conversationRepository.save(conversation);
Message message = new Message();
message.setConversationId(conversation.getId());
message.setSenderId(userId);
message.setContent(request.getMessage());
message.setIsRead(false);
messageRepository.save(message);
return ConversationResponse.fromEntity(conversation, request.getMessage());
}
public List<ConversationResponse> getConversations(Long userId, User.Role role) {
List<Conversation> conversations;
if (role == User.Role.CUSTOMER) {
conversations = conversationRepository.findByCustomerId(userId);
} else if (role == User.Role.STAFF) {
conversations = conversationRepository.findByStaffId(userId);
if (conversations.isEmpty()) {
conversations = conversationRepository.findAll();
}
} else {
conversations = conversationRepository.findAll();
}
return conversations.stream()
.map(conv -> {
List<Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conv.getId());
String lastMessage = messages.isEmpty() ? "" : messages.get(messages.size() - 1).getContent();
return ConversationResponse.fromEntity(conv, lastMessage);
})
.collect(Collectors.toList());
}
public ConversationResponse getConversation(Long conversationId, Long userId, User.Role role) {
Conversation conversation = conversationRepository.findById(conversationId)
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
if (role == User.Role.CUSTOMER && !conversation.getCustomerId().equals(userId)) {
throw new AccessDeniedException("You can only view your own conversations");
}
List<Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId);
String lastMessage = messages.isEmpty() ? "" : messages.get(messages.size() - 1).getContent();
return ConversationResponse.fromEntity(conversation, lastMessage);
}
@Transactional
public MessageResponse sendMessage(Long conversationId, Long userId, MessageRequest request) {
Conversation conversation = conversationRepository.findById(conversationId)
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
Message message = new Message();
message.setConversationId(conversationId);
message.setSenderId(userId);
message.setContent(request.getContent());
message.setIsRead(false);
message = messageRepository.save(message);
if (conversation.getStaffId() == null && !userId.equals(conversation.getCustomerId())) {
conversation.setStaffId(userId);
conversationRepository.save(conversation);
}
return MessageResponse.fromEntity(message);
}
public List<MessageResponse> getMessages(Long conversationId, Long userId, User.Role role) {
Conversation conversation = conversationRepository.findById(conversationId)
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
if (role == User.Role.CUSTOMER && !conversation.getCustomerId().equals(userId)) {
throw new AccessDeniedException("You can only view messages from your own conversations");
}
List<Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId);
return messages.stream()
.map(MessageResponse::fromEntity)
.collect(Collectors.toList());
}
}