Merge pull request #26 from SlimusMinus/fix-web_socket

fixed web-socket and add rewards on post
This commit is contained in:
SlimusMinus
2026-08-12 00:42:39 +03:00
committed by GitHub
12 changed files with 107 additions and 52 deletions

View File

@@ -1,8 +1,9 @@
package com.krylov.refound.config;
import com.krylov.refound.util.WsHandshakeInterceptor;
import com.krylov.refound.util.WsStompInterceptor;
import lombok.RequiredArgsConstructor;
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;
@@ -13,19 +14,23 @@ import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerCo
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final WsHandshakeInterceptor wsHandshakeInterceptor;
private final WsStompInterceptor wsStompInterceptor;
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic"); // сервер → клиент
registry.setApplicationDestinationPrefixes("/app"); // клиент → сервер
registry.enableSimpleBroker("/topic");
registry.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.addInterceptors(wsHandshakeInterceptor)
.withSockJS();
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(wsStompInterceptor);
}
}

View File

@@ -2,6 +2,7 @@ package com.krylov.refound.controller;
import com.krylov.refound.dto.SendMessageRequest;
import com.krylov.refound.service.ChatService;
import java.util.Map;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -14,25 +15,38 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class ChatWebSocketController {
private final ChatService chatService;
private final SimpMessagingTemplate messagingTemplate;
// Клиент шлёт сюда: /app/chat.send
@MessageMapping("/chat.send")
public void sendMessage(SendMessageRequest request, SimpMessageHeaderAccessor headerAccessor) {
Long senderId = (Long) Objects.requireNonNull(headerAccessor.getSessionAttributes()).get("userId");
if (senderId == null) {
log.warn("sendMessage: senderId is null, session attrs missing userId");
return; // или отправить ошибку клиенту
Map<String, Object> sessionAttributes = headerAccessor.getSessionAttributes();
if (sessionAttributes == null) {
log.warn("sendMessage: sessionAttributes is null");
return;
}
Object userIdObject = sessionAttributes.get("userId");
if (userIdObject == null) {
log.warn("sendMessage: userId missing in session attributes");
return;
}
Long senderId = ((Number) userIdObject).longValue();
if (request.getContent() == null || request.getContent().isBlank()) {
return;
}
try {
chatService.sendMessage(senderId, request.getChatId(), request.getContent());
} catch (SecurityException | IllegalArgumentException e) {
messagingTemplate.convertAndSendToUser(senderId.toString(), "/queue/errors", e.getMessage());
messagingTemplate.convertAndSendToUser(senderId.toString(), "/queue/errors", e.getMessage()
);
}
// ChatService сам отправит сообщение всем подписчикам через messagingTemplate
}
}

View File

@@ -54,10 +54,10 @@ public class PostController {
@PutMapping(value = "/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public PostResponse update(@PathVariable Long id, @RequestParam String type, @RequestParam String title, @RequestParam(required = false) String address,
@RequestParam(required = false) Double latitude, @RequestParam(required = false) Double longitude, @RequestParam(required = false) String description,
@RequestParam(required = false) String category, @RequestParam(required = false) String phone,
@RequestParam(required = false) String category, @RequestParam(required = false) String phone, @RequestParam(required = false) Boolean reward,
@RequestParam(value = "images", required = false) List<MultipartFile> images, @RequestParam(value = "existingImages", required = false) String existingImagesJson) {
log.info("update post {}", id);
return service.update(id, type, title, address, latitude, longitude, description, category, phone, images, existingImagesJson);
return service.update(id, type, title, address, latitude, longitude, description, category, phone, reward, images, existingImagesJson);
}
@DeleteMapping("/{id}")

View File

@@ -19,4 +19,5 @@ public class PostRequest {
private Double longitude;
private String address;
private String rulesAccepted;
private Boolean reward;
}

View File

@@ -27,4 +27,5 @@ public class PostResponse {
private Long likeCount;
private Role userRole;
private String rulesAccepted;
private Boolean reward;
}

View File

@@ -42,6 +42,7 @@ public class Post {
private LocalDateTime createdAt;
private String phone;
private String rulesAccepted;
private Boolean isReward;
@Enumerated(EnumType.STRING)
private PostType type;

View File

@@ -64,6 +64,7 @@ public class PostService {
post.setCreatedAt(LocalDateTime.now());
post.setStatus(PostStatus.MODERATION);
post.setUser(currentUser);
post.setIsReward(request.getReward());
if (request.getLatitude() == null || request.getLongitude() == null) {
post.setCity(request.getAddress());
} else {
@@ -134,7 +135,7 @@ public class PostService {
*/
@Transactional
public PostResponse update(Long id, String type, String title, String address, Double latitude,
Double longitude, String description, String category, String phone, List<MultipartFile> images, String existingImagesJson) {
Double longitude, String description, String category, String phone, Boolean reward, List<MultipartFile> images, String existingImagesJson) {
PostRequest request = new PostRequest();
request.setType(PostType.from(type));
@@ -145,6 +146,7 @@ public class PostService {
request.setLatitude(latitude);
request.setLongitude(longitude);
request.setAddress(address);
request.setReward(reward);
List<String> existingUrls = parseExistingImages(existingImagesJson);
@@ -163,6 +165,7 @@ public class PostService {
}
post.setPhone(request.getPhone());
post.setType(request.getType());
post.setIsReward(request.getReward());
if (request.getLatitude() != null && request.getLongitude() != null) {
post.setLatitude(request.getLatitude());
@@ -361,6 +364,7 @@ public class PostService {
PostResponse response = mapper.toResponse(post);
response.setLikeCount(likeCounts.getOrDefault(post.getId(), 0L));
response.setFavorite(userFavoriteIds.contains(post.getId()));
response.setReward(post.getIsReward());
if (includeUserRole) {
response.setUserRole(post.getUser().getRole());
}

View File

@@ -1,37 +0,0 @@
package com.krylov.refound.util;
import com.krylov.refound.security.JwtService;
import com.krylov.refound.service.UserService;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
@Component
@RequiredArgsConstructor
public class WsHandshakeInterceptor implements HandshakeInterceptor {
private final JwtService jwtService;
private final UserService userService;
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) {
if (request instanceof ServletServerHttpRequest servletRequest) {
String token = servletRequest.getServletRequest().getParameter("token");
if (token != null && jwtService.isTokenValid(token)) {
String login = jwtService.extractLogin(token);
Long userId = userService.getUserIdByLogin(login);
attributes.put("userId", userId);
return true;
}
}
return false; // отклонить подключение без валидного токена
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {}
}

View File

@@ -0,0 +1,53 @@
package com.krylov.refound.util;
import com.krylov.refound.security.JwtService;
import com.krylov.refound.service.UserService;
import lombok.RequiredArgsConstructor;
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.stereotype.Component;
@Component
@RequiredArgsConstructor
public class WsStompInterceptor implements ChannelInterceptor {
private final JwtService jwtService;
private final UserService userService;
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
StompHeaderAccessor.wrap(message);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String authorization = accessor.getFirstNativeHeader("Authorization");
if (authorization == null || !authorization.startsWith("Bearer ")) {
throw new IllegalArgumentException("Missing Authorization header");
}
String token = authorization.substring(7);
if (!jwtService.isTokenValid(token)) {
throw new IllegalArgumentException("Invalid JWT token");
}
String login = jwtService.extractLogin(token);
Long userId = userService.getUserIdByLogin(login);
if (userId == null) {
throw new IllegalArgumentException("User not found");
}
accessor.getSessionAttributes().put("userId", userId);
}
return message;
}
}

View File

@@ -54,4 +54,4 @@ jwt:
app:
cors:
allowed-origins: ${CORS_ALLOWED_ORIGINS:https://refound.example.com,http://localhost:3000}
allowed-origins: http://localhost:5173,http://localhost:3000

View File

@@ -0,0 +1,12 @@
databaseChangeLog:
- changeSet:
id: 014-add-reward-posts
author: alexandr krylov
changes:
- addColumn:
tableName: posts
columns:
- column:
name: is_reward
type: boolean
defaultValueBoolean: false

View File

@@ -18,3 +18,4 @@ databaseChangeLog:
- include: { file: db/changelog/add/013-add-rules_accepted-posts.yaml}
- include: { file: db/changelog/create/009-create-statistic.yaml }
- include: { file: db/changelog/alter/011-alter-posts-district-length.yaml}
- include: { file: db/changelog/add/014-add-reward-posts.yaml }