Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions src/main/java/com/ramsai/kitchen/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.requestMatchers("/api/v1/auth/**").permitAll()
// 2. Public Static Resources
.requestMatchers(
"/",
"/*.html",
"/static/**",
"/css/**",
"/js/**",
"/images/**",
"/uploads/**",
"/",
"/*.html",
"/static/**",
"/css/**",
"/js/**",
"/images/**",
"/uploads/**",
"/ws/**",
"/error"
).permitAll()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.ramsai.kitchen.config;

import com.ramsai.kitchen.services.JwtService;
import lombok.RequiredArgsConstructor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;

import java.security.Principal;

@Component
@RequiredArgsConstructor
public class WebSocketAuthChannelInterceptor implements ChannelInterceptor {

private static final String KITCHEN_TOPIC = "/topic/kitchen";

private final JwtService jwtService;
private final UserDetailsService userDetailsService;

@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (accessor == null) {
return message;
}

if (StompCommand.CONNECT.equals(accessor.getCommand())) {
accessor.setUser(authenticate(accessor.getFirstNativeHeader("Authorization")));
} else if (StompCommand.SUBSCRIBE.equals(accessor.getCommand())) {
String destination = accessor.getDestination();
if (destination != null && destination.startsWith(KITCHEN_TOPIC) && !hasKitchenRole(accessor.getUser())) {
throw new MessagingException("Not authorized to subscribe to the kitchen feed");
}
}

return message;
}

private UsernamePasswordAuthenticationToken authenticate(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new MessagingException("Missing Authorization header on STOMP CONNECT");
}
String jwt = authHeader.substring(7);
String username = jwtService.extractUsername(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (!jwtService.isTokenValid(jwt, userDetails)) {
throw new MessagingException("Invalid or expired token on STOMP CONNECT");
}
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}

private boolean hasKitchenRole(Principal principal) {
if (!(principal instanceof UsernamePasswordAuthenticationToken auth)) {
return false;
}
for (GrantedAuthority authority : auth.getAuthorities()) {
if ("ROLE_CHEF".equals(authority.getAuthority()) || "ROLE_MANAGER".equals(authority.getAuthority())) {
return true;
}
}
return false;
}
}
51 changes: 51 additions & 0 deletions src/main/java/com/ramsai/kitchen/config/WebSocketConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.ramsai.kitchen.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.simp.config.ChannelRegistration;
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;

import java.util.List;

@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

private final WebSocketAuthChannelInterceptor authChannelInterceptor;
private final ObjectMapper objectMapper;

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
}

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic");
registry.setApplicationDestinationPrefixes("/app");
}

@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(authChannelInterceptor);
}

// Reuse the Spring-managed ObjectMapper so payloads (e.g. LocalDateTime) serialize
// identically over STOMP and over REST.
@Override
public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setObjectMapper(objectMapper);
messageConverters.add(converter);
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import com.ramsai.kitchen.models.dtos.CartItemRequest;
import com.ramsai.kitchen.models.dtos.CartResponse;
import com.ramsai.kitchen.models.dtos.OrderResponse;
import com.ramsai.kitchen.models.entities.User;
import com.ramsai.kitchen.services.CartService;
import com.ramsai.kitchen.services.KitchenBroadcastService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
Expand All @@ -18,6 +20,7 @@
public class CartController {

private final CartService cartService;
private final KitchenBroadcastService kitchenBroadcastService;

@GetMapping
public ResponseEntity<Map<String, Object>> getCart(@AuthenticationPrincipal User user) {
Expand Down Expand Up @@ -54,7 +57,8 @@ public ResponseEntity<Map<String, Object>> removeItem(
public ResponseEntity<Map<String, Object>> checkout(
@AuthenticationPrincipal User user,
@RequestParam Long tableId) {
cartService.checkout(user.getId(), tableId);
OrderResponse order = cartService.checkout(user.getId(), tableId);
kitchenBroadcastService.broadcastOrderUpdate(order);
return ResponseEntity.ok(Map.of(
"message", "Checkout successful. Your order is being prepared!"
));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.ramsai.kitchen.controllers;

import com.ramsai.kitchen.enums.ItemStatus;
import com.ramsai.kitchen.enums.OrderStatus;
import com.ramsai.kitchen.models.dtos.OrderItemResponse;
import com.ramsai.kitchen.models.dtos.OrderResponse;
import com.ramsai.kitchen.services.KitchenBroadcastService;
import com.ramsai.kitchen.services.KitchenService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
Expand All @@ -16,6 +19,16 @@
public class KitchenController {

private final KitchenService kitchenService;
private final KitchenBroadcastService kitchenBroadcastService;

@GetMapping("/board")
public ResponseEntity<Map<String, Object>> getBoard() {
List<OrderResponse> data = kitchenService.getBoard();
return ResponseEntity.ok(Map.of(
"data", data,
"message", "Success"
));
}

@GetMapping("/items")
public ResponseEntity<Map<String, Object>> getKdsItems(@RequestParam ItemStatus status) {
Expand All @@ -30,9 +43,23 @@ public ResponseEntity<Map<String, Object>> getKdsItems(@RequestParam ItemStatus
public ResponseEntity<Map<String, Object>> updateItemStatus(
@PathVariable Long id,
@RequestParam ItemStatus status) {
kitchenService.updateItemStatus(id, status);
OrderResponse order = kitchenService.updateItemStatus(id, status);
kitchenBroadcastService.broadcastOrderUpdate(order);
return ResponseEntity.ok(Map.of(
"data", order,
"message", "Item status updated successfully"
));
}

@PatchMapping("/orders/{id}/status")
public ResponseEntity<Map<String, Object>> updateOrderStatus(
@PathVariable Long id,
@RequestParam OrderStatus status) {
OrderResponse order = kitchenService.advanceOrder(id, status);
kitchenBroadcastService.broadcastOrderUpdate(order);
return ResponseEntity.ok(Map.of(
"data", order,
"message", "Order status updated successfully"
));
}
}
14 changes: 14 additions & 0 deletions src/main/java/com/ramsai/kitchen/mappers/OrderMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.ramsai.kitchen.mappers;

import com.ramsai.kitchen.models.dtos.OrderResponse;
import com.ramsai.kitchen.models.entities.Order;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper(componentModel = "spring", uses = OrderItemMapper.class)
public interface OrderMapper {

@Mapping(target = "tableId", source = "table.id")
@Mapping(target = "tableNumber", source = "table.tableNumber")
OrderResponse toResponse(Order order);
}
19 changes: 19 additions & 0 deletions src/main/java/com/ramsai/kitchen/repositories/OrderRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,23 @@ public interface OrderRepository extends JpaRepository<Order, Long> {
"ORDER BY o.createdAt DESC"
)
List<Order> findCustomerOrders(Long customerId, OrderStatus excluded);

@org.springframework.data.jpa.repository.Query(
"SELECT DISTINCT o FROM Order o " +
"LEFT JOIN FETCH o.items i " +
"LEFT JOIN FETCH i.product " +
"LEFT JOIN FETCH o.table " +
"WHERE o.status IN :statuses " +
"ORDER BY o.createdAt ASC"
)
List<Order> findForKitchenByStatuses(List<OrderStatus> statuses);

@org.springframework.data.jpa.repository.Query(
"SELECT DISTINCT o FROM Order o " +
"LEFT JOIN FETCH o.items i " +
"LEFT JOIN FETCH i.product " +
"LEFT JOIN FETCH o.table " +
"WHERE o.id = :id"
)
Optional<Order> findByIdForKitchen(Long id);
}
6 changes: 5 additions & 1 deletion src/main/java/com/ramsai/kitchen/services/CartService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import com.ramsai.kitchen.enums.OrderStatus;
import com.ramsai.kitchen.enums.ItemStatus;
import com.ramsai.kitchen.mappers.OrderItemMapper;
import com.ramsai.kitchen.mappers.OrderMapper;
import com.ramsai.kitchen.models.dtos.CartItemRequest;
import com.ramsai.kitchen.models.dtos.CartResponse;
import com.ramsai.kitchen.models.dtos.OrderItemResponse;
import com.ramsai.kitchen.models.dtos.OrderResponse;
import com.ramsai.kitchen.models.entities.Order;
import com.ramsai.kitchen.models.entities.OrderItem;
import com.ramsai.kitchen.models.entities.Product;
Expand All @@ -32,6 +34,7 @@ public class CartService {
private final ProductRepository productRepository;
private final RestaurantTableRepository tableRepository;
private final OrderItemMapper orderItemMapper;
private final OrderMapper orderMapper;
private final InventoryService inventoryService;

@Transactional
Expand Down Expand Up @@ -88,7 +91,7 @@ public CartResponse removeItem(Long customerId, Long itemId) {
}

@Transactional
public void checkout(Long customerId, Long tableId) {
public OrderResponse checkout(Long customerId, Long tableId) {
Order cart = orderRepository.findByCustomerIdAndStatus(customerId, OrderStatus.DRAFT)
.orElseThrow(() -> new RuntimeException("No active cart found"));

Expand Down Expand Up @@ -116,6 +119,7 @@ public void checkout(Long customerId, Long tableId) {
cart.setStatus(OrderStatus.RECEIVED);
orderRepository.save(cart);
log.info("Order {} checked out for table {}", cart.getId(), tableId);
return orderMapper.toResponse(cart);
}

private Order getOrCreateDraftOrder(Long customerId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.ramsai.kitchen.services;

import com.ramsai.kitchen.models.dtos.OrderResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class KitchenBroadcastService {

private final SimpMessagingTemplate messagingTemplate;

// Pushes a freshly-saved order to the kitchen board and to anyone tracking that order.
// Called by controllers after the transaction commits.
public void broadcastOrderUpdate(OrderResponse order) {
if (order == null) {
return;
}
messagingTemplate.convertAndSend("/topic/kitchen", order);
messagingTemplate.convertAndSend("/topic/orders/" + order.id(), order);
}
}
Loading
Loading