Skip to content
Draft
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
17 changes: 0 additions & 17 deletions k8s/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,20 +77,3 @@ spec:
secretKeyRef:
name: redis-prod
key: redis-password

## livenessProbe:
# httpGet:
# path: /actuator/health
# port: 8080
# initialDelaySeconds: 120
# failureThreshold: 3
# timeoutSeconds: 5
# periodSeconds: 30
# readinessProbe:
# httpGet:
# path: /actuator/health
# port: 8080
# initialDelaySeconds: 30
# timeoutSeconds: 10
# failureThreshold: 5
# periodSeconds: 30
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import com.fredmaina.chatapp.Auth.Models.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; // Import Query
import org.springframework.data.repository.query.Param; // Import Param
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;
import java.util.UUID;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,6 @@ protected void doFilterInternal(jakarta.servlet.http.HttpServletRequest request,
UserDetails userDetails = userDetailsService.loadUserByUsername(username);

if (jwtService.isTokenValid(jwt)&& userDetails.isEnabled()) {
String action = jwtService.getClaimFromToken(jwt, "action");

if ("reset-password".equals(action)) {
if (!request.getRequestURI().startsWith("/api/auth/reset-password")) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("Invalid token for this operation");
return;
}
}
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,11 @@ public ResponseEntity<AuthResponse> register(@Valid @RequestBody SignUpRequest s
if ("Username already exists (case-insensitive)".equals(authResponse.getMessage()) || "Email already exists".equals(authResponse.getMessage())) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(authResponse);
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(authResponse); // General bad request for other issues
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(authResponse);
}
@PostMapping("/oauth/google")
public ResponseEntity<?> googleOAuth(@RequestBody GoogleOAuthRequest request) {
AuthResponse response = authService.handleGoogleOAuth(request.getCode(), request.getRedirectUri());
// log.error(response.toString()); // log.info or log.debug might be more appropriate for successful responses
if (response.isSuccess()) {
log.info("Google OAuth successful for user: {}", response.getUser() != null ? response.getUser().getEmail() : "Unknown");
return ResponseEntity.ok(response);
Expand All @@ -66,7 +65,7 @@ public ResponseEntity<?> googleOAuth(@RequestBody GoogleOAuthRequest request) {
@GetMapping("/me")
public ResponseEntity<AuthResponse> me(@RequestHeader("Authorization") String authHeader) {
String token = authHeader.replace("Bearer ", "");
String email = jwtService.getUsernameFromToken(token); // This actually gets the email (subject of token)
String email = jwtService.getUsernameFromToken(token);
User user = userRepository.findByEmail(email).orElse(null);

if (user == null) {
Expand All @@ -77,7 +76,7 @@ public ResponseEntity<AuthResponse> me(@RequestHeader("Authorization") String au
return ResponseEntity.ok(
AuthResponse.builder()
.success(true)
.token(token) // Consider if sending the token back is necessary here
.token(token)
.user(user)
.build());
}
Expand All @@ -95,11 +94,10 @@ public ResponseEntity<AuthResponse> setUsername(@RequestBody Map<String,String>
if(authResponse.isSuccess()){
return ResponseEntity.ok(authResponse);
}
// Distinguish between user not found and username taken
if ("Username already taken (case-insensitive)".equals(authResponse.getMessage())) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(authResponse);
}
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(authResponse); // Assuming "Invalid email" means user not found
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(authResponse);

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public Claims getClaimsFromToken(String token) {
}
public String getClaimFromToken(String token, String claimKey) {
Claims claims = getClaimsFromToken(token);
return claims.get(claimKey, String.class); // Get claim by key
return claims.get(claimKey, String.class);
}

public boolean isTokenValid(String token) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal; // Not used in current methods, but good for future
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*; // Added DeleteMapping
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID; // For UUID
import java.util.UUID;

@Slf4j
@Controller
Expand All @@ -29,7 +29,7 @@ public class ChatController {
@Autowired
private ChatService chatService;
@Autowired
private JWTService jwtService; // Corrected case from jWTService
private JWTService jwtService;
@Autowired
private UserRepository userRepository;
@Autowired
Expand Down Expand Up @@ -63,7 +63,7 @@ public ResponseEntity<?> getUserChats(@RequestHeader(value = "Authorization", re
@GetMapping("/chat/session_history")
public ResponseEntity<?> getAnonChatHistory(
@RequestParam String sessionId,
@RequestParam String recipient // This is the username of the dashboard owner
@RequestParam String recipient
) {
;
List<ChatMessageDto> messages = chatService.getChatHistoryForAnonymous(sessionId, recipient);
Expand Down Expand Up @@ -98,7 +98,6 @@ public ResponseEntity<?> deleteChatSession(
chatService.deleteChatSession(user.getId(), anonSessionId);
return ResponseEntity.ok(Map.of("success", true, "message", "Chat session deleted successfully."));
} catch (RuntimeException e) {
// Log the exception e
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("success", false, "message", "Failed to delete chat session: " + e.getMessage()));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public void afterConnectionEstablished(WebSocketSession session) {
for (String part : parts) {
String[] keyValue = part.trim().split("=");
if (keyValue.length == 2 && keyValue[0].equals("anonSessionId")) {
log.info("extracted from cookies");
log.debug("extracted from cookies");
}
}
}
Expand Down Expand Up @@ -108,7 +108,6 @@ protected void handleTextMessage(WebSocketSession session, TextMessage message)
}
}

// Helper to extract JWT from query params
private String extractUsernameFromJWT(WebSocketSession session) {
try {
URI uri = session.getUri();
Expand All @@ -128,31 +127,28 @@ private String extractUsernameFromJWT(WebSocketSession session) {
return null;
}

// Helper to extract session ID from cookies
private String extractAnonSessionId(WebSocketSession session) {
try {
// Try to get from cookies
List<String> cookies = session.getHandshakeHeaders().get("cookie");
if (cookies != null) {
for (String header : cookies) {
String[] parts = header.split(";");
for (String part : parts) {
String[] keyValue = part.trim().split("=");
if (keyValue.length == 2 && keyValue[0].equals("anonSessionId")) {
log.info("extracted form cookies");
log.debug("extracted from cookies");
return keyValue[1];
}
}
}
}
// Fallback: Try to get from URI query param
URI uri = session.getUri();
if (uri != null && uri.getQuery() != null) {
String[] queryParams = uri.getQuery().split("&");
for (String param : queryParams) {
String[] keyValue = param.split("=");
if (keyValue.length == 2 && keyValue[0].equals("anonSessionId")) {
log.info("extracted from URI");
log.debug("extracted from URI");
return keyValue[1];
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
public class ChatMessageDto {
private String id;
private String text;
private String senderType; // "anonymous" or "self"
private String senderType;
private String timestamp;
private String nickname;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public static ChatMessageDto toDto(ChatMessage msg, UUID currentUserId) {
.id(msg.getMessageId().toString())
.text(msg.getContent())
.senderType(isFromCurrentUser ? "self" : "anonymous")
.timestamp(formattedTimestamp) // Use formattedTimestamp
.timestamp(formattedTimestamp)
.nickname(isFromCurrentUser ? null : msg.getNickname())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import lombok.Getter;
import lombok.Setter;

import java.time.Instant; // Changed from LocalDateTime
import java.time.Instant;
import java.util.List;

@Builder
Expand All @@ -14,7 +14,7 @@ public class ChatSessionDto {
private String id;
private String senderNickname;
private String lastMessage;
private Instant lastMessageTimestamp; // Changed to Instant
private Instant lastMessageTimestamp;
private int unreadCount;
private String avatarUrl;
private List<ChatMessageDto> messages;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
@NoArgsConstructor
@ToString
public class WebSocketMessagePayload {
private MessageType type; // e.g., "ANON_TO_USER" or "USER_TO_ANON"
private String from; // username or sessionId
private String to; // username or sessionId
private MessageType type;
private String from;
private String to;
private String content;
private String nickname;
private String timestamp;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,8 @@

public interface ChatMessageRepository extends JpaRepository<ChatMessage, UUID> {

// Fetches messages TO a specific user (used as a base or for other features)
List<ChatMessage> findByToUser(User toUser);

// Fetches chat history for a specific anonymous session with a specific user (bidirectional)
// Used by /api/chat/session_history

@Query("SELECT m FROM ChatMessage m " +
"WHERE " +
"(m.fromSessionId = :sessionId AND m.toUser.id = :recipientId) " +
Expand All @@ -27,10 +23,6 @@ public interface ChatMessageRepository extends JpaRepository<ChatMessage, UUID>
List<ChatMessage> findFullChatHistory(@Param("sessionId") String sessionId,
@Param("recipientId") UUID recipientId);



// Finds distinct anonymous session IDs that have messaged a specific user
// Used by /api/chats to get all chat sessions for a user
@Query("SELECT DISTINCT m.fromSessionId FROM ChatMessage m " +
"WHERE m.toUser.id = :userId AND m.fromSessionId IS NOT NULL")
List<String> findDistinctSessionsByToUserId(@Param("userId") UUID userId);
Expand All @@ -39,18 +31,15 @@ List<ChatMessage> findFullChatHistory(@Param("sessionId") String sessionId,


@Query("SELECT m FROM ChatMessage m " +
"WHERE (m.fromSessionId = :sessionId AND m.toUser.id = :userId) " + // Anon to User
" OR (m.toSessionId = :sessionId AND m.fromUser.id = :userId) " + // User to Anon
"WHERE (m.fromSessionId = :sessionId AND m.toUser.id = :userId) " +
" OR (m.toSessionId = :sessionId AND m.fromUser.id = :userId) " +
"ORDER BY m.timestamp ASC")
List<ChatMessage> findAllMessagesByUserAndSession(@Param("userId") UUID userId, @Param("sessionId") String sessionId);


// Counts unread messages for a user from a specific anonymous session
@Query("SELECT COUNT(m) FROM ChatMessage m " +
"WHERE m.toUser.id = :userId AND m.fromSessionId = :fromSessionId AND m.isRead = false")
long countUnreadMessagesForSession(@Param("userId") UUID userId, @Param("fromSessionId") String fromSessionId);

// Marks messages from an anonymous session to a user as read
@Modifying
@Query("UPDATE ChatMessage m SET m.isRead = true " +
"WHERE m.toUser.id = :userId AND m.fromSessionId = :fromSessionId AND m.isRead = false")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ public List<ChatSessionDto> getUserChatSessions(UUID userId) {
.id(sessionId)
.senderNickname(senderNickname)
.lastMessage(lastMsg.getContent())
.lastMessageTimestamp(lastMsg.getTimestamp()) // This will now be Instant
.lastMessageTimestamp(lastMsg.getTimestamp())
.unreadCount((int) unreadCount)
.avatarUrl("https://i.pravatar.cc/150?u=" + sessionId) // Placeholder
.avatarUrl("https://i.pravatar.cc/150?u=" + sessionId)
.messages(
allMessagesInSession.stream()
.map(msg -> ChatMessageMapper.toDto(msg, userId))
Expand All @@ -66,7 +66,6 @@ public List<ChatSessionDto> getUserChatSessions(UUID userId) {
}
}

// Sort sessions by lastMessageTimestamp descending
sessions.sort(Comparator.comparing(ChatSessionDto::getLastMessageTimestamp, Comparator.nullsLast(Comparator.reverseOrder())));

return sessions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@ public void setMessageAsRead(String sessionId) {
chatMessageRepository.markMessagesAsRead(sessionId);
}

// For RedisSubscriber to call
public void deliverToSession(String to, String json, boolean isUser) {
WebSocketSession session = isUser
? userSessions.get(to)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class JacksonConfig {
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // optional, pretty ISO-8601
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
package com.fredmaina.chatapp.core.config;// Example for WebSocketConfig.java if choosing TextWebSocketHandler
package com.fredmaina.chatapp.core.config;

import com.fredmaina.chatapp.core.Controllers.ChatWebSocketHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.beans.factory.annotation.Autowired; // Autowire your handler
import org.springframework.beans.factory.annotation.Autowired;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Autowired
private ChatWebSocketHandler chatWebSocketHandler; // Your existing handler
private ChatWebSocketHandler chatWebSocketHandler;

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatWebSocketHandler, "/ws/chat")
.setAllowedOriginPatterns("*"); // Add .withSockJS() if still needed
.setAllowedOriginPatterns("*");
}
}
16 changes: 8 additions & 8 deletions src/main/java/com/fredmaina/chatapp/core/models/ChatMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import jakarta.persistence.*;
import lombok.*;

import java.time.Instant; // Changed from LocalDateTime
import java.time.Instant;
import java.util.UUID;

@Entity
Expand All @@ -22,22 +22,22 @@ public class ChatMessage {

private String content;

private String fromSessionId; // nullable: only set if it's from anonymous
private String fromSessionId;

private String toSessionId; // nullable: only set if it's to anonymous
private String toSessionId;

private String nickname; // optional: only needed if from anonymous
private String nickname;

private Instant timestamp; // Changed to Instant
private Instant timestamp;

@ManyToOne
@JoinColumn(name = "from_user_id")
private User fromUser; // nullable: only set if it's from a user
private User fromUser;

@ManyToOne
@JoinColumn(name = "to_user_id")
private User toUser; // nullable: only set if it's to a user
private User toUser;

@Column(nullable = false, columnDefinition = "BOOLEAN DEFAULT FALSE")
private boolean isRead = false; // New field, defaults to false
private boolean isRead = false;
}