diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..69c31df
Binary files /dev/null and b/.DS_Store differ
diff --git a/pom.xml b/pom.xml
index 88a43ce..d56828d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -79,12 +79,6 @@
4.2.0
-
-
- org.springframework.boot
- spring-boot-starter-websocket
-
-
org.telegram
diff --git a/src/main/java/com/lottery/lottery/config/WebConfig.java b/src/main/java/com/lottery/lottery/config/WebConfig.java
index 3305311..041c6fd 100644
--- a/src/main/java/com/lottery/lottery/config/WebConfig.java
+++ b/src/main/java/com/lottery/lottery/config/WebConfig.java
@@ -32,7 +32,6 @@ public class WebConfig implements WebMvcConfigurer {
"/api/check_user/**", // User check endpoint for external applications (open endpoint)
"/api/deposit_webhook/**", // 3rd party deposit completion webhook (token in path, no auth)
"/api/notify_broadcast/**", // Notify broadcast start/stop (token in path, no auth)
- "/api/remotebet/**", // Remote bet: token + feature switch protected, no user auth
"/api/admin/**" // Admin endpoints are handled by Spring Security
);
diff --git a/src/main/java/com/lottery/lottery/config/WebSocketAuthInterceptor.java b/src/main/java/com/lottery/lottery/config/WebSocketAuthInterceptor.java
deleted file mode 100644
index bb5a3c8..0000000
--- a/src/main/java/com/lottery/lottery/config/WebSocketAuthInterceptor.java
+++ /dev/null
@@ -1,106 +0,0 @@
-package com.lottery.lottery.config;
-
-import com.lottery.lottery.model.UserA;
-import com.lottery.lottery.security.UserContext;
-import com.lottery.lottery.service.SessionService;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.messaging.Message;
-import org.springframework.messaging.MessageChannel;
-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.stereotype.Component;
-
-import java.security.Principal;
-import java.util.List;
-
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class WebSocketAuthInterceptor implements ChannelInterceptor {
-
- private final SessionService sessionService;
-
- @Override
- public Message> preSend(Message> message, MessageChannel channel) {
- StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
-
- if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
- // Extract Bearer token from headers
- List authHeaders = accessor.getNativeHeader("Authorization");
- String token = null;
-
- if (authHeaders != null && !authHeaders.isEmpty()) {
- String authHeader = authHeaders.get(0);
- if (authHeader != null && authHeader.startsWith("Bearer ")) {
- token = authHeader.substring(7);
- }
- }
-
- // Also check query parameter (for SockJS fallback)
- if (token == null) {
- String query = accessor.getFirstNativeHeader("query");
- if (query != null && query.contains("token=")) {
- int tokenStart = query.indexOf("token=") + 6;
- int tokenEnd = query.indexOf("&", tokenStart);
- if (tokenEnd == -1) {
- tokenEnd = query.length();
- }
- token = query.substring(tokenStart, tokenEnd);
- }
- }
-
- if (token == null || token.isBlank()) {
- log.warn("WebSocket connection rejected: No token provided");
- throw new SecurityException("Authentication required");
- }
-
- // Validate token and get user
- var userOpt = sessionService.getUserBySession(token);
- if (userOpt.isEmpty()) {
- log.warn("WebSocket connection rejected: Invalid token");
- throw new SecurityException("Invalid authentication token");
- }
-
- UserA user = userOpt.get();
- accessor.setUser(new StompPrincipal(user.getId(), user));
- UserContext.set(user);
-
- log.debug("WebSocket connection authenticated for user {}", user.getId());
- }
-
- return message;
- }
-
- @Override
- public void postSend(Message> message, MessageChannel channel, boolean sent) {
- UserContext.clear();
- }
-
- // Simple principal class to store user info
- public static class StompPrincipal implements Principal {
- private final Integer userId;
- private final UserA user;
-
- public StompPrincipal(Integer userId, UserA user) {
- this.userId = userId;
- this.user = user;
- }
-
- public Integer getUserId() {
- return userId;
- }
-
- public UserA getUser() {
- return user;
- }
-
- @Override
- public String getName() {
- return String.valueOf(userId);
- }
- }
-}
-
diff --git a/src/main/java/com/lottery/lottery/config/WebSocketConfig.java b/src/main/java/com/lottery/lottery/config/WebSocketConfig.java
deleted file mode 100644
index e53ad4f..0000000
--- a/src/main/java/com/lottery/lottery/config/WebSocketConfig.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package com.lottery.lottery.config;
-
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.context.annotation.Configuration;
-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.Arrays;
-import java.util.List;
-
-@Slf4j
-@Configuration
-@EnableWebSocketMessageBroker
-public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
-
- private final WebSocketAuthInterceptor authInterceptor;
-
- @Value("${app.websocket.allowed-origins:https://win-spin.live,https://lottery-fe-production.up.railway.app,https://web.telegram.org,https://webk.telegram.org,https://webz.telegram.org}")
- private String allowedOrigins;
-
- public WebSocketConfig(WebSocketAuthInterceptor authInterceptor) {
- this.authInterceptor = authInterceptor;
- }
-
- @Override
- public void configureMessageBroker(MessageBrokerRegistry config) {
- // Enable simple broker for sending messages to clients
- config.enableSimpleBroker("/topic", "/queue");
- // Prefix for messages from client to server
- config.setApplicationDestinationPrefixes("/app");
- }
-
- @Override
- public void registerStompEndpoints(StompEndpointRegistry registry) {
- // Parse allowed origins from configuration
- // Spring's setAllowedOriginPatterns uses Ant-style patterns, not regex
- // For exact matches, use the URL as-is
- // For subdomain matching, use https://*.example.com
- List origins = Arrays.asList(allowedOrigins.split(","));
- String[] originPatterns = origins.stream()
- .map(String::trim)
- .filter(origin -> !origin.isEmpty())
- .toArray(String[]::new);
-
- log.info("[WEBSOCKET] Configuring WebSocket endpoint /ws with allowed origins: {}", Arrays.toString(originPatterns));
-
- // WebSocket endpoint - clients connect here
- registry.addEndpoint("/ws")
- .setAllowedOriginPatterns(originPatterns) // Restricted to configured domains
- .withSockJS();
- }
-
- @Override
- public void configureClientInboundChannel(ChannelRegistration registration) {
- registration.interceptors(authInterceptor);
- }
-}
-
diff --git a/src/main/java/com/lottery/lottery/config/WebSocketSubscriptionListener.java b/src/main/java/com/lottery/lottery/config/WebSocketSubscriptionListener.java
deleted file mode 100644
index 1a4ddef..0000000
--- a/src/main/java/com/lottery/lottery/config/WebSocketSubscriptionListener.java
+++ /dev/null
@@ -1,178 +0,0 @@
-package com.lottery.lottery.config;
-
-import com.lottery.lottery.dto.GameRoomStateDto;
-import com.lottery.lottery.service.GameRoomService;
-import com.lottery.lottery.service.RoomConnectionService;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.context.event.EventListener;
-import org.springframework.messaging.simp.SimpMessagingTemplate;
-import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
-import org.springframework.stereotype.Component;
-import org.springframework.web.socket.messaging.SessionDisconnectEvent;
-import org.springframework.web.socket.messaging.SessionSubscribeEvent;
-import org.springframework.web.socket.messaging.SessionUnsubscribeEvent;
-
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class WebSocketSubscriptionListener {
-
- private final GameRoomService gameRoomService;
- private final SimpMessagingTemplate messagingTemplate;
- private final RoomConnectionService roomConnectionService;
-
- // Pattern to match room subscription: /topic/room/{roomNumber}
- private static final Pattern ROOM_SUBSCRIPTION_PATTERN = Pattern.compile("/topic/room/(\\d+)");
-
- /**
- * Listens for WebSocket subscription events.
- * When a client subscribes to a room topic, sends the current room state immediately.
- */
- @EventListener
- public void handleSubscription(SessionSubscribeEvent event) {
- StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
- String destination = accessor.getDestination();
-
- if (destination == null) {
- return;
- }
-
- // Check if this is a room subscription
- Matcher matcher = ROOM_SUBSCRIPTION_PATTERN.matcher(destination);
- if (matcher.matches()) {
- try {
- Integer roomNumber = Integer.parseInt(matcher.group(1));
-
- // Get the user ID from the principal
- Object principal = accessor.getUser();
- Integer userId = null;
- if (principal instanceof WebSocketAuthInterceptor.StompPrincipal) {
- userId = ((WebSocketAuthInterceptor.StompPrincipal) principal).getUserId();
- }
-
- // Get session ID
- String sessionId = accessor.getSessionId();
-
- log.info("Client subscribed to room {} (userId: {}, sessionId: {})", roomNumber, userId, sessionId);
-
- // Register session for disconnect tracking
- if (sessionId != null && userId != null) {
- roomConnectionService.registerSession(sessionId, userId);
- }
-
- // Track room-level connection (not just round participation)
- if (userId != null && sessionId != null) {
- roomConnectionService.addUserToRoom(userId, roomNumber, sessionId);
- } else {
- log.warn("Cannot track room connection: userId={}, sessionId={}", userId, sessionId);
- }
-
- // Get current room state and send it to the subscribing client
- // This ensures client gets authoritative state immediately on subscribe
- GameRoomStateDto state = gameRoomService.getRoomState(roomNumber);
-
- // Send state directly to the destination (room topic)
- // This will be received by the subscribing client
- messagingTemplate.convertAndSend(destination, state);
-
- log.debug("Sent initial room state to subscriber: room={}, phase={}, participants={}, connectedUsers={}",
- roomNumber, state.getPhase(),
- state.getParticipants() != null ? state.getParticipants().size() : 0,
- state.getConnectedUsers());
-
- } catch (NumberFormatException e) {
- log.warn("Invalid room number in subscription destination: {}", destination);
- } catch (Exception e) {
- log.error("Error sending initial state for room subscription: {}", destination, e);
- }
- }
- }
-
- /**
- * Listens for WebSocket unsubscribe events.
- * When a client unsubscribes from a room topic, removes them from room connections.
- */
- @EventListener
- public void handleUnsubscribe(SessionUnsubscribeEvent event) {
- StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
- String destination = accessor.getDestination();
-
- // Skip if destination is null (Spring WebSocket sometimes sends unsubscribe events without destination during cleanup)
- if (destination == null) {
- return;
- }
-
- log.debug("Unsubscribe event received for destination: {}", destination);
-
- // Check if this is a room unsubscription
- Matcher matcher = ROOM_SUBSCRIPTION_PATTERN.matcher(destination);
- if (matcher.matches()) {
- try {
- Integer roomNumber = Integer.parseInt(matcher.group(1));
-
- // Get the user ID from the principal
- Object principal = accessor.getUser();
- Integer userId = null;
- if (principal instanceof WebSocketAuthInterceptor.StompPrincipal) {
- userId = ((WebSocketAuthInterceptor.StompPrincipal) principal).getUserId();
- } else {
- log.warn("Unsubscribe event: principal is not StompPrincipal, type: {}",
- principal != null ? principal.getClass().getName() : "null");
- }
-
- // Get session ID
- String sessionId = accessor.getSessionId();
-
- if (userId != null && sessionId != null) {
- log.info("Client unsubscribed from room {} (userId: {}, sessionId: {})", roomNumber, userId, sessionId);
- roomConnectionService.removeUserFromRoom(userId, roomNumber, sessionId);
- } else {
- log.warn("Unsubscribe event: userId or sessionId is null for destination: {} (userId: {}, sessionId: {})",
- destination, userId, sessionId);
- }
- } catch (NumberFormatException e) {
- log.warn("Invalid room number in unsubscription destination: {}", destination);
- } catch (Exception e) {
- log.error("Error handling room unsubscription: {}", destination, e);
- }
- } else {
- log.debug("Unsubscribe event destination does not match room pattern: {}", destination);
- }
- }
-
- /**
- * Listens for WebSocket disconnect events.
- * When a client disconnects completely, removes them from all rooms.
- */
- @EventListener
- public void handleDisconnect(SessionDisconnectEvent event) {
- StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
- String sessionId = accessor.getSessionId();
-
- // Try to get user ID from principal first
- Object principal = accessor.getUser();
- Integer userId = null;
- if (principal instanceof WebSocketAuthInterceptor.StompPrincipal) {
- userId = ((WebSocketAuthInterceptor.StompPrincipal) principal).getUserId();
- }
-
- if (userId != null && sessionId != null) {
- log.info("Client disconnected (userId: {}, sessionId: {}), removing session from all rooms", userId, sessionId);
- // Remove only this specific session from all rooms
- roomConnectionService.removeUserFromAllRooms(userId, sessionId);
- // Also remove session mapping
- roomConnectionService.removeSession(sessionId);
- } else if (sessionId != null) {
- // Principal might be lost, try to get userId from session mapping
- log.info("Client disconnected (sessionId: {}), principal lost, using session mapping", sessionId);
- roomConnectionService.removeUserFromAllRoomsBySession(sessionId);
- } else {
- log.warn("Disconnect event: both userId and sessionId are null, cannot remove from rooms");
- }
- }
-}
-
diff --git a/src/main/java/com/lottery/lottery/controller/AdminAnalyticsController.java b/src/main/java/com/lottery/lottery/controller/AdminAnalyticsController.java
index e2b222b..c9064b5 100644
--- a/src/main/java/com/lottery/lottery/controller/AdminAnalyticsController.java
+++ b/src/main/java/com/lottery/lottery/controller/AdminAnalyticsController.java
@@ -2,7 +2,6 @@ package com.lottery.lottery.controller;
import com.lottery.lottery.model.Payment;
import com.lottery.lottery.model.Payout;
-import com.lottery.lottery.repository.GameRoundRepository;
import com.lottery.lottery.repository.PaymentRepository;
import com.lottery.lottery.repository.PayoutRepository;
import com.lottery.lottery.repository.UserARepository;
@@ -30,7 +29,6 @@ public class AdminAnalyticsController {
private final UserARepository userARepository;
private final PaymentRepository paymentRepository;
private final PayoutRepository payoutRepository;
- private final GameRoundRepository gameRoundRepository;
/**
* Get revenue and payout time series data for charts.
@@ -181,14 +179,11 @@ public class AdminAnalyticsController {
// Count active players (logged in) in this period
long activePlayers = userARepository.countByDateLoginBetween(periodStartTs, periodEndTs);
- // Count rounds resolved in this period
- long rounds = gameRoundRepository.countByResolvedAtBetween(current, periodEnd);
-
Map point = new HashMap<>();
point.put("date", current.getEpochSecond());
point.put("newUsers", newUsers);
point.put("activePlayers", activePlayers);
- point.put("rounds", rounds);
+ point.put("rounds", 0L);
dataPoints.add(point);
diff --git a/src/main/java/com/lottery/lottery/controller/AdminBotConfigController.java b/src/main/java/com/lottery/lottery/controller/AdminBotConfigController.java
deleted file mode 100644
index 3bf508d..0000000
--- a/src/main/java/com/lottery/lottery/controller/AdminBotConfigController.java
+++ /dev/null
@@ -1,96 +0,0 @@
-package com.lottery.lottery.controller;
-
-import com.lottery.lottery.dto.AdminBotConfigDto;
-import com.lottery.lottery.dto.AdminBotConfigRequest;
-import com.lottery.lottery.service.AdminBotConfigService;
-import com.lottery.lottery.service.ConfigurationService;
-import lombok.RequiredArgsConstructor;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
-
-import jakarta.validation.Valid;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-
-@RestController
-@RequestMapping("/api/admin/bots")
-@RequiredArgsConstructor
-@PreAuthorize("hasRole('ADMIN')")
-public class AdminBotConfigController {
-
- private final AdminBotConfigService adminBotConfigService;
- private final ConfigurationService configurationService;
-
- @GetMapping
- public ResponseEntity> list() {
- return ResponseEntity.ok(adminBotConfigService.listAll());
- }
-
- @GetMapping("/{id}")
- public ResponseEntity getById(@PathVariable Integer id) {
- Optional dto = adminBotConfigService.getById(id);
- return dto.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
- }
-
- @PostMapping
- public ResponseEntity> create(@Valid @RequestBody AdminBotConfigRequest request) {
- try {
- AdminBotConfigDto created = adminBotConfigService.create(request);
- return ResponseEntity.status(HttpStatus.CREATED).body(created);
- } catch (IllegalArgumentException e) {
- return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
- }
- }
-
- @PutMapping("/{id}")
- public ResponseEntity> update(@PathVariable Integer id, @Valid @RequestBody AdminBotConfigRequest request) {
- try {
- Optional updated = adminBotConfigService.update(id, request);
- return updated.map(ResponseEntity::ok)
- .orElseGet(() -> ResponseEntity.notFound().build());
- } catch (IllegalArgumentException e) {
- return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
- }
- }
-
- @DeleteMapping("/{id}")
- public ResponseEntity delete(@PathVariable Integer id) {
- boolean deleted = adminBotConfigService.delete(id);
- return deleted ? ResponseEntity.noContent().build() : ResponseEntity.notFound().build();
- }
-
- /**
- * Shuffle time windows for bots that have the given room enabled.
- * Redistributes the same set of time windows randomly across those bots.
- */
- @PostMapping("/shuffle")
- public ResponseEntity> shuffleTimeWindows(@RequestParam int roomNumber) {
- if (roomNumber != 2 && roomNumber != 3) {
- return ResponseEntity.badRequest().body(Map.of("error", "roomNumber must be 2 or 3"));
- }
- try {
- adminBotConfigService.shuffleTimeWindowsForRoom(roomNumber);
- return ResponseEntity.ok().build();
- } catch (IllegalArgumentException e) {
- return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
- }
- }
-
- @GetMapping("/settings")
- public ResponseEntity