diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/src/main/java/de/w665/testing/config/UsernameHandshakeInterceptor.java b/src/main/java/de/w665/testing/config/UsernameHandshakeInterceptor.java deleted file mode 100644 index dbf66d2..0000000 --- a/src/main/java/de/w665/testing/config/UsernameHandshakeInterceptor.java +++ /dev/null @@ -1,37 +0,0 @@ -package de.w665.testing.config; - -import org.springframework.http.server.ServerHttpRequest; -import org.springframework.http.server.ServerHttpResponse; -import org.springframework.web.socket.WebSocketHandler; -import org.springframework.web.socket.server.HandshakeInterceptor; - -import java.net.URI; -import java.util.List; -import java.util.Map; - -public class UsernameHandshakeInterceptor implements HandshakeInterceptor { - @Override - public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) { - URI uri = request.getURI(); - String query = uri.getQuery(); - if (query != null) { - for (String pair : query.split("&")) { - String[] kv = pair.split("=", 2); - if (kv.length == 2 && kv[0].equals("token")) { - attributes.put("token", kv[1]); - } - } - } - // Also check headers (not used by SockJS handshake, but harmless) - List header = request.getHeaders().get("X-Auth-Token"); - if (header != null && !header.isEmpty()) { - attributes.put("token", header.getFirst()); - } - return true; - } - - @Override - public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { - // no-op - } -} diff --git a/src/main/java/de/w665/testing/config/UsernamePrincipalHandshakeHandler.java b/src/main/java/de/w665/testing/config/UsernamePrincipalHandshakeHandler.java deleted file mode 100644 index e33179d..0000000 --- a/src/main/java/de/w665/testing/config/UsernamePrincipalHandshakeHandler.java +++ /dev/null @@ -1,34 +0,0 @@ -package de.w665.testing.config; - -import de.w665.testing.service.TokenService; -import org.springframework.http.server.ServerHttpRequest; -import org.springframework.web.socket.WebSocketHandler; -import org.springframework.web.socket.server.support.DefaultHandshakeHandler; - -import java.security.Principal; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; - -public class UsernamePrincipalHandshakeHandler extends DefaultHandshakeHandler { - private final TokenService tokenService; - - public UsernamePrincipalHandshakeHandler(TokenService tokenService) { - this.tokenService = tokenService; - } - - @Override - protected Principal determineUser(ServerHttpRequest request, - WebSocketHandler wsHandler, - Map attributes) { - Object tokenObj = attributes.get("token"); - String token = tokenObj != null ? tokenObj.toString() : null; - if (token == null) { - // Anonymous session (reject by generating random, effectively not mapped to any user) - return () -> "anon-" + UUID.randomUUID(); - } - Optional user = tokenService.resolveUsername(token); - return user.map(name -> () -> name) - .orElseGet(() -> (()-> "anon-" + UUID.randomUUID())); - } -} diff --git a/src/main/java/de/w665/testing/config/WebSocketConfig.java b/src/main/java/de/w665/testing/config/WebSocketConfig.java index 231f309..6110b2b 100644 --- a/src/main/java/de/w665/testing/config/WebSocketConfig.java +++ b/src/main/java/de/w665/testing/config/WebSocketConfig.java @@ -5,30 +5,19 @@ 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 de.w665.testing.service.TokenService; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { - private final TokenService tokenService; - - public WebSocketConfig(TokenService tokenService) { - this.tokenService = tokenService; - } - - @Override - public void configureMessageBroker(MessageBrokerRegistry config) { - config.enableSimpleBroker("/topic", "/queue"); - config.setApplicationDestinationPrefixes("/app"); - config.setUserDestinationPrefix("/user"); - } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/ws") - .setAllowedOriginPatterns("*") - .addInterceptors(new UsernameHandshakeInterceptor()) - .setHandshakeHandler(new UsernamePrincipalHandshakeHandler(tokenService)) - .withSockJS(); + registry.addEndpoint("/ws").withSockJS(); + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.setApplicationDestinationPrefixes("/app"); + registry.enableSimpleBroker("/topic"); } } diff --git a/src/main/java/de/w665/testing/controller/AuthController.java b/src/main/java/de/w665/testing/controller/AuthController.java deleted file mode 100644 index 65bf1d1..0000000 --- a/src/main/java/de/w665/testing/controller/AuthController.java +++ /dev/null @@ -1,40 +0,0 @@ -package de.w665.testing.controller; - -import de.w665.testing.dto.SignupRequest; -import de.w665.testing.dto.SignupResponse; -import de.w665.testing.service.PresenceService; -import de.w665.testing.service.TokenService; -import de.w665.testing.service.UserService; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.Set; - -@RestController -@RequestMapping("/api") -public class AuthController { - private final UserService userService; - private final PresenceService presenceService; - private final TokenService tokenService; - - public AuthController(UserService userService, PresenceService presenceService, TokenService tokenService) { - this.userService = userService; - this.presenceService = presenceService; - this.tokenService = tokenService; - } - - @PostMapping("/signup") - public ResponseEntity signup(@RequestBody SignupRequest request) { - try { - String token = userService.signup(request.getUsername()); - return ResponseEntity.ok(new SignupResponse(request.getUsername().trim().toLowerCase(), token)); - } catch (IllegalArgumentException ex) { - return ResponseEntity.status(409).body(ex.getMessage()); - } - } - - @GetMapping("/online") - public Set online() { - return presenceService.getOnlineUsers(); - } -} diff --git a/src/main/java/de/w665/testing/controller/ChatController.java b/src/main/java/de/w665/testing/controller/ChatController.java new file mode 100644 index 0000000..6ab3a4b --- /dev/null +++ b/src/main/java/de/w665/testing/controller/ChatController.java @@ -0,0 +1,27 @@ +package de.w665.testing.controller; + +import de.w665.testing.model.ChatMessage; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.stereotype.Controller; + +@Controller +public class ChatController { + + @MessageMapping("/chat.sendMessage") + @SendTo("/topic/public") + public ChatMessage sendMessage(@Payload ChatMessage chatMessage) { + return chatMessage; + } + + @MessageMapping("/chat.addUser") + @SendTo("/topic/public") + public ChatMessage addUser(@Payload ChatMessage chatMessage, + SimpMessageHeaderAccessor headerAccessor) { + // Add username in web socket session + headerAccessor.getSessionAttributes().put("username", chatMessage.getSender()); + return chatMessage; + } +} diff --git a/src/main/java/de/w665/testing/controller/WebSocketChatController.java b/src/main/java/de/w665/testing/controller/WebSocketChatController.java deleted file mode 100644 index 8cae98f..0000000 --- a/src/main/java/de/w665/testing/controller/WebSocketChatController.java +++ /dev/null @@ -1,29 +0,0 @@ -package de.w665.testing.controller; - -import de.w665.testing.dto.ChatMessage; -import de.w665.testing.dto.SendMessageCommand; -import org.springframework.messaging.handler.annotation.MessageMapping; -import org.springframework.messaging.handler.annotation.Payload; -import org.springframework.messaging.simp.SimpMessagingTemplate; -import org.springframework.stereotype.Controller; - -import java.security.Principal; - -@Controller -public class WebSocketChatController { - private final SimpMessagingTemplate messagingTemplate; - - public WebSocketChatController(SimpMessagingTemplate messagingTemplate) { - this.messagingTemplate = messagingTemplate; - } - - @MessageMapping("/chat/send") - public void send(@Payload SendMessageCommand cmd, Principal principal) { - if (principal == null || cmd.getRoomId() == null || cmd.getRoomId().isBlank() || cmd.getContent() == null || cmd.getContent().isBlank()) { - return; - } - String sender = principal.getName(); - ChatMessage msg = ChatMessage.of(cmd.getRoomId(), sender, cmd.getContent()); - messagingTemplate.convertAndSend("/topic/room." + cmd.getRoomId(), msg); - } -} diff --git a/src/main/java/de/w665/testing/controller/WebSocketInviteController.java b/src/main/java/de/w665/testing/controller/WebSocketInviteController.java deleted file mode 100644 index a8f5a2e..0000000 --- a/src/main/java/de/w665/testing/controller/WebSocketInviteController.java +++ /dev/null @@ -1,45 +0,0 @@ -package de.w665.testing.controller; - -import de.w665.testing.dto.AcceptInviteCommand; -import de.w665.testing.dto.DeclineInviteCommand; -import de.w665.testing.dto.InviteCommand; -import de.w665.testing.service.InviteService; -import org.springframework.messaging.handler.annotation.MessageMapping; -import org.springframework.messaging.handler.annotation.Payload; -import org.springframework.stereotype.Controller; - -import java.security.Principal; - -@Controller -public class WebSocketInviteController { - private final InviteService inviteService; - - public WebSocketInviteController(InviteService inviteService) { - this.inviteService = inviteService; - } - - @MessageMapping("/invite/send") - public void sendInvite(@Payload InviteCommand cmd, Principal principal) { - if (principal == null) return; - String from = principal.getName(); - if (cmd.getTo() == null || cmd.getTo().isBlank()) return; - if (from.equalsIgnoreCase(cmd.getTo())) return; - inviteService.sendInvite(from, cmd.getTo().trim().toLowerCase()); - } - - @MessageMapping("/invite/accept") - public void acceptInvite(@Payload AcceptInviteCommand cmd, Principal principal) { - if (principal == null) return; - String recipient = principal.getName(); - if (cmd.getInviter() == null || cmd.getInviter().isBlank()) return; - inviteService.acceptInvite(recipient, cmd.getInviter().trim().toLowerCase()); - } - - @MessageMapping("/invite/decline") - public void declineInvite(@Payload DeclineInviteCommand cmd, Principal principal) { - if (principal == null) return; - String recipient = principal.getName(); - if (cmd.getInviter() == null || cmd.getInviter().isBlank()) return; - inviteService.declineInvite(recipient, cmd.getInviter().trim().toLowerCase()); - } -} diff --git a/src/main/java/de/w665/testing/dto/AcceptInviteCommand.java b/src/main/java/de/w665/testing/dto/AcceptInviteCommand.java deleted file mode 100644 index 1cf378a..0000000 --- a/src/main/java/de/w665/testing/dto/AcceptInviteCommand.java +++ /dev/null @@ -1,13 +0,0 @@ -package de.w665.testing.dto; - -public class AcceptInviteCommand { - private String inviter; - - public String getInviter() { - return inviter; - } - - public void setInviter(String inviter) { - this.inviter = inviter; - } -} diff --git a/src/main/java/de/w665/testing/dto/ChatMessage.java b/src/main/java/de/w665/testing/dto/ChatMessage.java deleted file mode 100644 index 2f4415b..0000000 --- a/src/main/java/de/w665/testing/dto/ChatMessage.java +++ /dev/null @@ -1,55 +0,0 @@ -package de.w665.testing.dto; - -import java.time.Instant; - -public class ChatMessage { - private String roomId; - private String sender; - private String content; - private long timestamp; - - public ChatMessage() {} - - public ChatMessage(String roomId, String sender, String content, long timestamp) { - this.roomId = roomId; - this.sender = sender; - this.content = content; - this.timestamp = timestamp; - } - - public static ChatMessage of(String roomId, String sender, String content) { - return new ChatMessage(roomId, sender, content, Instant.now().toEpochMilli()); - } - - public String getRoomId() { - return roomId; - } - - public void setRoomId(String roomId) { - this.roomId = roomId; - } - - public String getSender() { - return sender; - } - - public void setSender(String sender) { - this.sender = sender; - } - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - - public long getTimestamp() { - return timestamp; - } - - public void setTimestamp(long timestamp) { - this.timestamp = timestamp; - } -} diff --git a/src/main/java/de/w665/testing/dto/DeclineInviteCommand.java b/src/main/java/de/w665/testing/dto/DeclineInviteCommand.java deleted file mode 100644 index a4fc420..0000000 --- a/src/main/java/de/w665/testing/dto/DeclineInviteCommand.java +++ /dev/null @@ -1,13 +0,0 @@ -package de.w665.testing.dto; - -public class DeclineInviteCommand { - private String inviter; - - public String getInviter() { - return inviter; - } - - public void setInviter(String inviter) { - this.inviter = inviter; - } -} diff --git a/src/main/java/de/w665/testing/dto/InviteCommand.java b/src/main/java/de/w665/testing/dto/InviteCommand.java deleted file mode 100644 index 239812b..0000000 --- a/src/main/java/de/w665/testing/dto/InviteCommand.java +++ /dev/null @@ -1,13 +0,0 @@ -package de.w665.testing.dto; - -public class InviteCommand { - private String to; - - public String getTo() { - return to; - } - - public void setTo(String to) { - this.to = to; - } -} diff --git a/src/main/java/de/w665/testing/dto/InviteEvent.java b/src/main/java/de/w665/testing/dto/InviteEvent.java deleted file mode 100644 index f99f05f..0000000 --- a/src/main/java/de/w665/testing/dto/InviteEvent.java +++ /dev/null @@ -1,63 +0,0 @@ -package de.w665.testing.dto; - -public class InviteEvent { - public enum Type { INVITE, ACCEPT, DECLINE } - - private Type type; - private String from; - private String to; - private String roomId; // present when type == ACCEPT - - public InviteEvent() {} - - public InviteEvent(Type type, String from, String to, String roomId) { - this.type = type; - this.from = from; - this.to = to; - this.roomId = roomId; - } - - public static InviteEvent invite(String from, String to) { - return new InviteEvent(Type.INVITE, from, to, null); - } - - public static InviteEvent accept(String from, String to, String roomId) { - return new InviteEvent(Type.ACCEPT, from, to, roomId); - } - - public static InviteEvent decline(String from, String to) { - return new InviteEvent(Type.DECLINE, from, to, null); - } - - public Type getType() { - return type; - } - - public void setType(Type type) { - this.type = type; - } - - public String getFrom() { - return from; - } - - public void setFrom(String from) { - this.from = from; - } - - public String getTo() { - return to; - } - - public void setTo(String to) { - this.to = to; - } - - public String getRoomId() { - return roomId; - } - - public void setRoomId(String roomId) { - this.roomId = roomId; - } -} diff --git a/src/main/java/de/w665/testing/dto/SendMessageCommand.java b/src/main/java/de/w665/testing/dto/SendMessageCommand.java deleted file mode 100644 index 1683ef9..0000000 --- a/src/main/java/de/w665/testing/dto/SendMessageCommand.java +++ /dev/null @@ -1,22 +0,0 @@ -package de.w665.testing.dto; - -public class SendMessageCommand { - private String roomId; - private String content; - - public String getRoomId() { - return roomId; - } - - public void setRoomId(String roomId) { - this.roomId = roomId; - } - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } -} diff --git a/src/main/java/de/w665/testing/dto/SignupRequest.java b/src/main/java/de/w665/testing/dto/SignupRequest.java deleted file mode 100644 index 0b18ea1..0000000 --- a/src/main/java/de/w665/testing/dto/SignupRequest.java +++ /dev/null @@ -1,13 +0,0 @@ -package de.w665.testing.dto; - -public class SignupRequest { - private String username; - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } -} diff --git a/src/main/java/de/w665/testing/dto/SignupResponse.java b/src/main/java/de/w665/testing/dto/SignupResponse.java deleted file mode 100644 index 1fcb45d..0000000 --- a/src/main/java/de/w665/testing/dto/SignupResponse.java +++ /dev/null @@ -1,29 +0,0 @@ -package de.w665.testing.dto; - -public class SignupResponse { - private String username; - private String token; - - public SignupResponse() {} - - public SignupResponse(String username, String token) { - this.username = username; - this.token = token; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getToken() { - return token; - } - - public void setToken(String token) { - this.token = token; - } -} diff --git a/src/main/java/de/w665/testing/model/ChatMessage.java b/src/main/java/de/w665/testing/model/ChatMessage.java new file mode 100644 index 0000000..db64a34 --- /dev/null +++ b/src/main/java/de/w665/testing/model/ChatMessage.java @@ -0,0 +1,11 @@ +package de.w665.testing.model; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ChatMessage { + private String content; + private String sender; +} diff --git a/src/main/java/de/w665/testing/service/ChatRoomService.java b/src/main/java/de/w665/testing/service/ChatRoomService.java deleted file mode 100644 index 4b6e658..0000000 --- a/src/main/java/de/w665/testing/service/ChatRoomService.java +++ /dev/null @@ -1,28 +0,0 @@ -package de.w665.testing.service; - -import org.springframework.stereotype.Service; - -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - -@Service -public class ChatRoomService { - // key: canonicalPair(userA,userB) -> roomId - private final Map pairToRoom = new ConcurrentHashMap<>(); - - public String getOrCreateRoom(String userA, String userB) { - String key = canonicalPair(userA, userB); - return pairToRoom.computeIfAbsent(key, k -> "room-" + Math.abs(k.hashCode())); - } - - public Optional getRoomIfExists(String userA, String userB) { - return Optional.ofNullable(pairToRoom.get(canonicalPair(userA, userB))); - } - - private String canonicalPair(String a, String b) { - String u1 = a.toLowerCase(); - String u2 = b.toLowerCase(); - return (u1.compareTo(u2) <= 0) ? (u1 + "|" + u2) : (u2 + "|" + u1); - } -} diff --git a/src/main/java/de/w665/testing/service/InviteService.java b/src/main/java/de/w665/testing/service/InviteService.java deleted file mode 100644 index 9310d08..0000000 --- a/src/main/java/de/w665/testing/service/InviteService.java +++ /dev/null @@ -1,48 +0,0 @@ -package de.w665.testing.service; - -import de.w665.testing.dto.InviteEvent; -import org.springframework.messaging.simp.SimpMessagingTemplate; -import org.springframework.stereotype.Service; - -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -@Service -public class InviteService { - private final SimpMessagingTemplate messagingTemplate; - private final ChatRoomService chatRoomService; - - // recipient -> set of inviters - private final Map> pending = new ConcurrentHashMap<>(); - - public InviteService(SimpMessagingTemplate messagingTemplate, ChatRoomService chatRoomService) { - this.messagingTemplate = messagingTemplate; - this.chatRoomService = chatRoomService; - } - - public void sendInvite(String from, String to) { - pending.computeIfAbsent(to, k -> ConcurrentHashMap.newKeySet()).add(from); - InviteEvent event = InviteEvent.invite(from, to); - messagingTemplate.convertAndSendToUser(to, "/queue/invites", event); - } - - public void acceptInvite(String recipient, String inviter) { - Set inviters = pending.getOrDefault(recipient, ConcurrentHashMap.newKeySet()); - if (inviters.remove(inviter)) { - String room = chatRoomService.getOrCreateRoom(inviter, recipient); - InviteEvent acceptForInviter = InviteEvent.accept(recipient, inviter, room); - InviteEvent acceptForRecipient = InviteEvent.accept(inviter, recipient, room); - messagingTemplate.convertAndSendToUser(inviter, "/queue/invites", acceptForInviter); - messagingTemplate.convertAndSendToUser(recipient, "/queue/invites", acceptForRecipient); - } - } - - public void declineInvite(String recipient, String inviter) { - Set inviters = pending.getOrDefault(recipient, ConcurrentHashMap.newKeySet()); - if (inviters.remove(inviter)) { - InviteEvent declineForInviter = InviteEvent.decline(recipient, inviter); - messagingTemplate.convertAndSendToUser(inviter, "/queue/invites", declineForInviter); - } - } -} diff --git a/src/main/java/de/w665/testing/service/PresenceService.java b/src/main/java/de/w665/testing/service/PresenceService.java deleted file mode 100644 index 484562b..0000000 --- a/src/main/java/de/w665/testing/service/PresenceService.java +++ /dev/null @@ -1,54 +0,0 @@ -package de.w665.testing.service; - -import org.springframework.messaging.simp.SimpMessagingTemplate; -import org.springframework.stereotype.Service; - -import java.util.Collections; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -@Service -public class PresenceService { - private final SimpMessagingTemplate messagingTemplate; - - // sessionId -> username - private final Map sessionToUser = new ConcurrentHashMap<>(); - // username -> session count - private final Map userSessionCounts = new ConcurrentHashMap<>(); - - public PresenceService(SimpMessagingTemplate messagingTemplate) { - this.messagingTemplate = messagingTemplate; - } - - public void userConnected(String sessionId, String username) { - sessionToUser.put(sessionId, username); - userSessionCounts.merge(username, 1, Integer::sum); - broadcastOnlineUsers(); - } - - public void userDisconnected(String sessionId) { - String username = sessionToUser.remove(sessionId); - if (username != null) { - userSessionCounts.computeIfPresent(username, (u, count) -> { - int next = count - 1; - return next <= 0 ? null : next; - }); - broadcastOnlineUsers(); - } - } - - public Set getOnlineUsers() { - return Collections.unmodifiableSet( - userSessionCounts.entrySet().stream() - .filter(e -> e.getValue() != null && e.getValue() > 0) - .map(Map.Entry::getKey) - .collect(Collectors.toSet()) - ); - } - - public void broadcastOnlineUsers() { - messagingTemplate.convertAndSend("/topic/online-users", getOnlineUsers()); - } -} diff --git a/src/main/java/de/w665/testing/service/TokenService.java b/src/main/java/de/w665/testing/service/TokenService.java deleted file mode 100644 index 2a9042d..0000000 --- a/src/main/java/de/w665/testing/service/TokenService.java +++ /dev/null @@ -1,35 +0,0 @@ -package de.w665.testing.service; - -import org.springframework.stereotype.Service; - -import java.security.SecureRandom; -import java.util.Base64; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - -@Service -public class TokenService { - private final Map tokenToUser = new ConcurrentHashMap<>(); - private final SecureRandom random = new SecureRandom(); - - public String createTokenForUsername(String username) { - String token = generateToken(); - tokenToUser.put(token, username); - return token; - } - - public Optional resolveUsername(String token) { - return Optional.ofNullable(tokenToUser.get(token)); - } - - public void invalidate(String token) { - tokenToUser.remove(token); - } - - private String generateToken() { - byte[] bytes = new byte[32]; - random.nextBytes(bytes); - return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); - } -} diff --git a/src/main/java/de/w665/testing/service/UserService.java b/src/main/java/de/w665/testing/service/UserService.java deleted file mode 100644 index 0b50413..0000000 --- a/src/main/java/de/w665/testing/service/UserService.java +++ /dev/null @@ -1,41 +0,0 @@ -package de.w665.testing.service; - -import org.springframework.stereotype.Service; - -import java.util.Collections; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -@Service -public class UserService { - private final TokenService tokenService; - private final Map usernameToToken = new ConcurrentHashMap<>(); - - public UserService(TokenService tokenService) { - this.tokenService = tokenService; - } - - public synchronized String signup(String username) { - String normalized = normalize(username); - if (usernameToToken.containsKey(normalized)) { - throw new IllegalArgumentException("Username already taken"); - } - String token = tokenService.createTokenForUsername(normalized); - usernameToToken.put(normalized, token); - return token; - } - - public Optional getTokenFor(String username) { - return Optional.ofNullable(usernameToToken.get(normalize(username))); - } - - public Set getAllRegisteredUsers() { - return Collections.unmodifiableSet(usernameToToken.keySet()); - } - - private String normalize(String username) { - return username.trim().toLowerCase(); - } -} diff --git a/src/main/java/de/w665/testing/ws/WebSocketPresenceEventListener.java b/src/main/java/de/w665/testing/ws/WebSocketPresenceEventListener.java deleted file mode 100644 index 3fb8566..0000000 --- a/src/main/java/de/w665/testing/ws/WebSocketPresenceEventListener.java +++ /dev/null @@ -1,54 +0,0 @@ -package de.w665.testing.ws; - -import de.w665.testing.service.PresenceService; -import org.springframework.context.event.EventListener; -import org.springframework.messaging.simp.stomp.StompHeaderAccessor; -import org.springframework.stereotype.Component; -import org.springframework.web.socket.messaging.SessionConnectEvent; -import org.springframework.web.socket.messaging.SessionConnectedEvent; -import org.springframework.web.socket.messaging.SessionDisconnectEvent; - -import java.security.Principal; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -@Component -public class WebSocketPresenceEventListener { - private final PresenceService presenceService; - private final Set connectedSessions = ConcurrentHashMap.newKeySet(); - - public WebSocketPresenceEventListener(PresenceService presenceService) { - this.presenceService = presenceService; - } - - @EventListener - public void handleWebSocketConnectListener(SessionConnectEvent event) { - StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage()); - Principal p = sha.getUser(); - String sessionId = sha.getSessionId(); - if (p != null && sessionId != null && connectedSessions.add(sessionId)) { - presenceService.userConnected(sessionId, p.getName()); - } - } - - @EventListener - public void handleWebSocketConnectedListener(SessionConnectedEvent event) { - StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage()); - Principal p = sha.getUser(); - String sessionId = sha.getSessionId(); - if (p != null && sessionId != null && connectedSessions.add(sessionId)) { - presenceService.userConnected(sessionId, p.getName()); - } - } - - @EventListener - public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) { - String sessionId = event.getSessionId(); - if (sessionId != null && connectedSessions.remove(sessionId)) { - presenceService.userDisconnected(sessionId); - } else { - // Even if we didn't mark it as connected (edge cases), attempt to notify presence - presenceService.userDisconnected(sessionId); - } - } -} diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index badec66..56c48b8 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -1,286 +1,168 @@ - + - - - Simple Chat - - - + QChat + + -
-

Simple Chat

- -
-
-
-

Sign up

-
- - -
- -

Online users

-
    -
    -
    -

    Chat

    -
    -
    -
    -
    - - -
    +
    +
    +
    +
    +
    +
    +

    Enter Your Name

    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +

    QChat

    +
    +
      +
      +
      + +
      + +
      +
      +
      +
      -
      + +