From ff36b2e18db395831e1758f6ff68e19aa2192faf Mon Sep 17 00:00:00 2001 From: bravin Date: Tue, 18 Feb 2025 20:29:19 -0500 Subject: [PATCH] CHAT-53: Fixes to improve error messages from cloudflare. --- backend-service/pom.xml | 8 + .../co/teamsphere/api/DTO/ChatSummaryDTO.java | 23 ++- .../co/teamsphere/api/DTO/MessageDTO.java | 4 + .../api/controller/AuthController.java | 65 ++++--- .../api/exception/CloudflareException.java | 9 + .../api/repository/ChatRepository.java | 30 +++- .../api/response/CloudflareApiResponse.java | 3 +- .../api/response/ErrorResponse.java | 25 +++ .../api/services/AuthenticationService.java | 8 +- .../impl/AuthenticationServiceImpl.java | 168 ++++++++---------- .../api/services/impl/ChatServiceImpl.java | 66 ++----- .../impl/CloudflareApiServiceImpl.java | 77 +++++--- .../src/main/resources/application-local.yml | 2 +- pom.xml | 10 ++ 14 files changed, 293 insertions(+), 205 deletions(-) create mode 100644 backend-service/src/main/java/co/teamsphere/api/exception/CloudflareException.java create mode 100644 backend-service/src/main/java/co/teamsphere/api/response/ErrorResponse.java diff --git a/backend-service/pom.xml b/backend-service/pom.xml index 693ab77..c4b4f03 100644 --- a/backend-service/pom.xml +++ b/backend-service/pom.xml @@ -138,6 +138,14 @@ org.springframework.cloud spring-cloud-starter-bootstrap + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-core + org.mockito mockito-core diff --git a/backend-service/src/main/java/co/teamsphere/api/DTO/ChatSummaryDTO.java b/backend-service/src/main/java/co/teamsphere/api/DTO/ChatSummaryDTO.java index 03e9d22..18c4cfa 100644 --- a/backend-service/src/main/java/co/teamsphere/api/DTO/ChatSummaryDTO.java +++ b/backend-service/src/main/java/co/teamsphere/api/DTO/ChatSummaryDTO.java @@ -1,16 +1,35 @@ package co.teamsphere.api.DTO; -import lombok.Builder; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; +import java.time.LocalDateTime; import java.util.UUID; @Data -@Builder +@AllArgsConstructor +@NoArgsConstructor public class ChatSummaryDTO { private UUID id; private String chatName; private String chatImage; private UUID createdBy; private MessageDTO lastMessage; + + public ChatSummaryDTO(UUID id, String chatName, String chatImage, UUID createdBy, + UUID messageId, String content, LocalDateTime timeStamp, + boolean isRead, UUID userId, UUID chatId, + String otherUserName, String otherUserProfile) { + this.id = id; + this.chatName = chatName; + this.chatImage = chatImage; + this.createdBy = createdBy; + this.lastMessage = (messageId != null) + ? new MessageDTO(messageId, content, timeStamp, isRead, userId, chatId) + : null; + this.chatName = otherUserName; + this.chatImage = otherUserProfile; + } } + diff --git a/backend-service/src/main/java/co/teamsphere/api/DTO/MessageDTO.java b/backend-service/src/main/java/co/teamsphere/api/DTO/MessageDTO.java index 49c0b3e..a885cfb 100644 --- a/backend-service/src/main/java/co/teamsphere/api/DTO/MessageDTO.java +++ b/backend-service/src/main/java/co/teamsphere/api/DTO/MessageDTO.java @@ -1,13 +1,17 @@ package co.teamsphere.api.DTO; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import java.time.LocalDateTime; import java.util.UUID; @Data @Builder +@AllArgsConstructor +@NoArgsConstructor public class MessageDTO { private UUID id; private String content; diff --git a/backend-service/src/main/java/co/teamsphere/api/controller/AuthController.java b/backend-service/src/main/java/co/teamsphere/api/controller/AuthController.java index 903ca47..e8d4ed8 100644 --- a/backend-service/src/main/java/co/teamsphere/api/controller/AuthController.java +++ b/backend-service/src/main/java/co/teamsphere/api/controller/AuthController.java @@ -8,6 +8,7 @@ import co.teamsphere.api.request.LoginRequest; import co.teamsphere.api.request.SignupRequest; import co.teamsphere.api.response.AuthResponse; +import co.teamsphere.api.response.ErrorResponse; import co.teamsphere.api.services.AuthenticationService; import co.teamsphere.api.utils.GoogleAuthRequest; import co.teamsphere.api.utils.GoogleUserInfo; @@ -19,6 +20,7 @@ import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; +import java.time.Instant; import java.time.ZoneOffset; import java.util.UUID; import org.springframework.http.HttpStatus; @@ -93,30 +95,33 @@ public ResponseEntity verifyJwtToken() { ), @ApiResponse(responseCode = "400", description = "Invalid input or user already exists") }) - @PostMapping(value="/signup", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity userSignupMethod ( + @PostMapping(value = "/signup", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity userSignupMethod( @Schema(description = "User details", implementation = SignupRequest.class) - @Valid @ModelAttribute SignupRequest request) throws UserException, ProfileImageException { - try { - log.info("Processing signup request for user with email: {}, username:{}", request.getEmail(), request.getUsername()); + @Valid @ModelAttribute SignupRequest request) { - AuthResponse authResponse = authenticationService.signupUser(request); + log.info("Processing signup request for user with email: {}, username: {}", request.getEmail(), request.getUsername()); + try { + AuthResponse authResponse = authenticationService.signupUser(request); log.info("Signup process completed successfully for user with email: {}", request.getEmail()); + return ResponseEntity.status(HttpStatus.CREATED).body(authResponse); - return new ResponseEntity<>(authResponse, HttpStatus.CREATED); } catch (UserException e) { - log.error("Error during signup process", e); - throw e; // Rethrow specific exception to be handled by global exception handler - } catch (ProfileImageException e){ - log.warn("File type not accepted, {}", request.getFile().getContentType()); - throw new ProfileImageException("Profile Picture type is not allowed!"); + log.error("User-related error during signup process", e); + return buildErrorResponse(HttpStatus.BAD_REQUEST, e.getMessage(), "/signup"); + + } catch (ProfileImageException e) { + log.warn("File type not accepted: {}", request.getFile().getContentType()); + return buildErrorResponse(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "Profile picture type is not allowed!", "/signup"); + } catch (Exception e) { log.error("Unexpected error during signup process", e); - throw new UserException("Unexpected error during signup process"); + return buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected error during signup process", "/signup"); } } + @Operation(summary = "Login a user", description = "Login with email and password.") @ApiResponses(value = { @ApiResponse( @@ -130,27 +135,29 @@ public ResponseEntity userSignupMethod ( @ApiResponse(responseCode = "401", description = "Invalid credentials") }) @PostMapping("/login") - public ResponseEntity userLoginMethod( + public ResponseEntity userLoginMethod( @Schema(description = "Login request body", implementation = LoginRequest.class) - @Valid @RequestBody LoginRequest loginRequest) throws UserException { - try { - log.info("Processing login request for user with username: {}", loginRequest.getEmail()); + @Valid @RequestBody LoginRequest loginRequest) { - AuthResponse authResponse = authenticationService.loginUser(loginRequest.getEmail(), loginRequest.getPassword()); + log.info("Processing login request for user with username: {}", loginRequest.getEmail()); + try { + AuthResponse authResponse = authenticationService.loginUser(loginRequest.getEmail(), loginRequest.getPassword()); log.info("Login successful for user with username: {}", loginRequest.getEmail()); + return ResponseEntity.ok(authResponse); - return new ResponseEntity<>(authResponse, HttpStatus.OK); } catch (BadCredentialsException e) { log.warn("Authentication failed for user with username: {}", loginRequest.getEmail()); - throw new UserException("Invalid username or password."); + return buildErrorResponse(HttpStatus.UNAUTHORIZED, "Invalid username or password.", "/login"); + } catch (Exception e) { log.error("Unexpected error during login process", e); - throw new UserException("Unexpected error during login process."); + return buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected error during login process.", "/login"); } } - @Transactional // move business logic to service layer + //TODO: move business logic to service layer + @Transactional @Operation(summary = "Authenticate via Google", description = "login/signup via Google OAuth.") @ApiResponses(value = { @ApiResponse(responseCode = "200", @@ -213,4 +220,18 @@ public ResponseEntity authenticateWithGoogleMethod( return new ResponseEntity<>(new AuthResponse("Error during Google authentication: " + e.getMessage(), false), HttpStatus.INTERNAL_SERVER_ERROR); } } + private ResponseEntity buildErrorResponse(HttpStatus status, String message, String endpoint) { + ErrorResponse errorResponse = new ErrorResponse( + new ErrorResponse.ErrorDetails( + status.value(), + message, + "An error occurred while processing the request.", + endpoint, + "POST", + Instant.now().toString(), + UUID.randomUUID().toString() // Unique request ID for tracking + ) + ); + return ResponseEntity.status(status).body(errorResponse); + } } diff --git a/backend-service/src/main/java/co/teamsphere/api/exception/CloudflareException.java b/backend-service/src/main/java/co/teamsphere/api/exception/CloudflareException.java new file mode 100644 index 0000000..1c4ff5d --- /dev/null +++ b/backend-service/src/main/java/co/teamsphere/api/exception/CloudflareException.java @@ -0,0 +1,9 @@ +package co.teamsphere.api.exception; + +import lombok.Data; + +@Data +public class CloudflareException { + private int code; + private String message; +} diff --git a/backend-service/src/main/java/co/teamsphere/api/repository/ChatRepository.java b/backend-service/src/main/java/co/teamsphere/api/repository/ChatRepository.java index 6ccd77e..2990c32 100644 --- a/backend-service/src/main/java/co/teamsphere/api/repository/ChatRepository.java +++ b/backend-service/src/main/java/co/teamsphere/api/repository/ChatRepository.java @@ -1,5 +1,6 @@ package co.teamsphere.api.repository; +import co.teamsphere.api.DTO.ChatSummaryDTO; import co.teamsphere.api.models.Chat; import co.teamsphere.api.models.User; import org.springframework.data.domain.Page; @@ -14,10 +15,31 @@ @Repository public interface ChatRepository extends JpaRepository { - - @Query("SELECT c FROM Chat c JOIN c.users u WHERE u.id = :userId") - Page findChatsByUserId(@Param("userId") UUID userId, Pageable pageable); - + @Query(""" + SELECT new co.teamsphere.api.DTO.ChatSummaryDTO( + c.id, + c.chatName, + c.chatImage, + c.createdBy.id, + m.id, + m.content, + m.timeStamp, + m.isRead, + m.username.id, + m.chat.id, + COALESCE((SELECT u.username FROM c.users u WHERE u.id <> :userId AND c.isGroup = false), ''), + COALESCE((SELECT u.profilePicture FROM c.users u WHERE u.id <> :userId AND c.isGroup = false), '') + ) + FROM Chat c + JOIN c.users u + LEFT JOIN c.messages m ON m.timeStamp = ( + SELECT MAX(m2.timeStamp) FROM Messages m2 WHERE m2.chat.id = c.id + ) + WHERE u.id = :userId + GROUP BY c, m.id, m.content, m.isRead, m.username.id, m.chat.id + ORDER BY MAX(m.timeStamp) DESC +""") + Page findChatsByUserId(@Param("userId") UUID userId, Pageable pageable); @Query("select c from Chat c Where c.isGroup=false And :user Member of c.users And :reqUser Member of c.users") Chat findSingleChatByUsersId(@Param("user") User user, @Param("reqUser") User reqUser); diff --git a/backend-service/src/main/java/co/teamsphere/api/response/CloudflareApiResponse.java b/backend-service/src/main/java/co/teamsphere/api/response/CloudflareApiResponse.java index e928ee6..7bf8f12 100644 --- a/backend-service/src/main/java/co/teamsphere/api/response/CloudflareApiResponse.java +++ b/backend-service/src/main/java/co/teamsphere/api/response/CloudflareApiResponse.java @@ -1,5 +1,6 @@ package co.teamsphere.api.response; +import co.teamsphere.api.exception.CloudflareException; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Getter; @@ -18,7 +19,7 @@ public class CloudflareApiResponse { private Result result; private boolean success; - private List errors; + private List errors; private List messages; @Getter diff --git a/backend-service/src/main/java/co/teamsphere/api/response/ErrorResponse.java b/backend-service/src/main/java/co/teamsphere/api/response/ErrorResponse.java new file mode 100644 index 0000000..53fb802 --- /dev/null +++ b/backend-service/src/main/java/co/teamsphere/api/response/ErrorResponse.java @@ -0,0 +1,25 @@ +package co.teamsphere.api.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class ErrorResponse { + private ErrorDetails error; + + @Getter + @Setter + @AllArgsConstructor + public static class ErrorDetails { + private int status; + private String message; + private String details; + private String endpoint; + private String method; + private String timestamp; + private String requestId; + } +} diff --git a/backend-service/src/main/java/co/teamsphere/api/services/AuthenticationService.java b/backend-service/src/main/java/co/teamsphere/api/services/AuthenticationService.java index 1f29d88..46764f7 100644 --- a/backend-service/src/main/java/co/teamsphere/api/services/AuthenticationService.java +++ b/backend-service/src/main/java/co/teamsphere/api/services/AuthenticationService.java @@ -1,17 +1,17 @@ package co.teamsphere.api.services; -import org.springframework.stereotype.Service; - import co.teamsphere.api.exception.ProfileImageException; import co.teamsphere.api.exception.UserException; import co.teamsphere.api.request.SignupRequest; import co.teamsphere.api.response.AuthResponse; - import jakarta.validation.Valid; +import org.springframework.stereotype.Service; + +import java.io.IOException; @Service public interface AuthenticationService { - AuthResponse signupUser(@Valid SignupRequest request) throws UserException, ProfileImageException; + AuthResponse signupUser(@Valid SignupRequest request) throws UserException, ProfileImageException, IOException; AuthResponse loginUser(String username, String password) throws UserException; } diff --git a/backend-service/src/main/java/co/teamsphere/api/services/impl/AuthenticationServiceImpl.java b/backend-service/src/main/java/co/teamsphere/api/services/impl/AuthenticationServiceImpl.java index fa5d9d6..8b42d23 100644 --- a/backend-service/src/main/java/co/teamsphere/api/services/impl/AuthenticationServiceImpl.java +++ b/backend-service/src/main/java/co/teamsphere/api/services/impl/AuthenticationServiceImpl.java @@ -1,21 +1,7 @@ package co.teamsphere.api.services.impl; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.Objects; -import java.util.regex.Pattern; - -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.validation.annotation.Validated; - import co.teamsphere.api.config.JWTTokenProvider; +import co.teamsphere.api.exception.CloudflareException; import co.teamsphere.api.exception.ProfileImageException; import co.teamsphere.api.exception.UserException; import co.teamsphere.api.models.User; @@ -25,9 +11,24 @@ import co.teamsphere.api.response.CloudflareApiResponse; import co.teamsphere.api.services.AuthenticationService; import co.teamsphere.api.services.CloudflareApiService; - import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Objects; +import java.util.regex.Pattern; +import java.util.stream.Collectors; @Service @Validated @@ -57,93 +58,80 @@ public AuthenticationServiceImpl( @Override @Transactional - public AuthResponse signupUser(@Valid SignupRequest request) throws UserException, ProfileImageException { - try { - if (isEmailInvalid(request.getEmail())) { - log.warn("Bad Email={} was passed in", request.getEmail()); - throw new UserException("Valid email was not passed in"); - } - - // Check if user with the given email or username already exists - if (userRepository.findByEmail(request.getEmail()).isPresent()) { - log.warn("Email={} is already used with another account", request.getEmail()); - throw new UserException("Email is already used with another account"); - } - - if (userRepository.findByUsername(request.getUsername()).isPresent()) { - log.warn("Username={} is already used with another account", request.getUsername()); - throw new UserException("Username is already used with another account"); - } - - if (request.getFile().isEmpty() || (!request.getFile().getContentType().equals("image/jpeg") && !request.getFile().getContentType().equals("image/png"))) { - log.warn("File type not accepted, {}", request.getFile().getContentType()); - throw new ProfileImageException("Profile Picture type is not allowed!"); - } - - // Upload profile picture to Cloudflare - CloudflareApiResponse responseEntity = cloudflareApiService.uploadImage(request.getFile()); - String baseUrl = Objects.requireNonNull(responseEntity.getResult().getVariants().get(0)); - String profileUrl = baseUrl.substring(0, baseUrl.lastIndexOf("/") + 1) + "public"; - - var currentDateTime = LocalDateTime.now().atOffset(ZoneOffset.UTC); - - // Creating a new user - var newUser = User.builder() - .email(request.getEmail()) - .username(request.getUsername()) - .password(passwordEncoder.encode(request.getPassword())) - .profilePicture(profileUrl) - .createdDate(currentDateTime) - .lastUpdatedDate(currentDateTime) - .build(); - - userRepository.save(newUser); - - // auto-login after signup - Authentication authentication = new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()); - SecurityContextHolder.getContext().setAuthentication(authentication); - String token = jwtTokenProvider.generateJwtToken(authentication); - - return new AuthResponse(token, true); + public AuthResponse signupUser(@Valid SignupRequest request) throws UserException, ProfileImageException, IOException { + if (isEmailInvalid(request.getEmail())) { + log.warn("Bad Email={} was passed in", request.getEmail()); + throw new UserException("Valid email was not passed in"); } - catch (UserException e) { - // TODO: think about returning a response and not throwing an error in a catch block - log.error("Error during signup process", e); - throw e; // Rethrow specific exception to be handled by global exception handler + + // Check if user with the given email or username already exists + if (userRepository.findByEmail(request.getEmail()).isPresent()) { + log.warn("Email={} is already used with another account", request.getEmail()); + throw new UserException("Email is already used with another account"); } - catch (ProfileImageException e){ - log.error("ERROR: {}", e.getMessage()); - throw new ProfileImageException(e.getMessage()); + + if (userRepository.findByUsername(request.getUsername()).isPresent()) { + log.warn("Username={} is already used with another account", request.getUsername()); + throw new UserException("Username is already used with another account"); + } + + if (request.getFile().isEmpty() || (!Objects.equals(request.getFile().getContentType(), "image/jpeg") && !Objects.equals(request.getFile().getContentType(), "image/png"))) { + log.warn("File type not accepted, {}", request.getFile().getContentType()); + throw new ProfileImageException("Profile Picture type is not allowed!"); } - catch (Exception e) { - log.error("Unexpected error during signup process", e); - throw new UserException("Unexpected error during signup process"); + + // Upload profile picture to Cloudflare + CloudflareApiResponse responseEntity = cloudflareApiService.uploadImage(request.getFile()); + + // Check if the Cloudflare API call was unsuccessful + if (!responseEntity.isSuccess() || (responseEntity.getErrors() != null && !responseEntity.getErrors().isEmpty())) { + String errorMessage = responseEntity.getErrors().stream() + .map(CloudflareException::getMessage) + .collect(Collectors.joining(", ")); + throw new ProfileImageException("Cloudflare upload failed: " + errorMessage); } + + + String baseUrl = Objects.requireNonNull(responseEntity.getResult().getVariants().get(0)); + String profileUrl = baseUrl.substring(0, baseUrl.lastIndexOf("/") + 1) + "public"; + + var currentDateTime = LocalDateTime.now().atOffset(ZoneOffset.UTC); + + // Creating a new user + var newUser = User.builder() + .email(request.getEmail()) + .username(request.getUsername()) + .password(passwordEncoder.encode(request.getPassword())) + .profilePicture(profileUrl) + .createdDate(currentDateTime) + .lastUpdatedDate(currentDateTime) + .build(); + + userRepository.save(newUser); + + // auto-login after signup + Authentication authentication = new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()); + SecurityContextHolder.getContext().setAuthentication(authentication); + String token = jwtTokenProvider.generateJwtToken(authentication); + + return new AuthResponse(token, true); } @Override @Transactional public AuthResponse loginUser(String email, String password) throws UserException { - try { - if(isEmailInvalid(email)){ - log.warn("Email={} is already used with another account", email); - throw new UserException("Email is already used with another account"); - } + if(isEmailInvalid(email)){ + log.warn("Email={} is already used with another account", email); + throw new UserException("Email is already used with another account"); + } - Authentication authentication = authentication(email, password); + Authentication authentication = authentication(email, password); - SecurityContextHolder.getContext().setAuthentication(authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); - String token = jwtTokenProvider.generateJwtToken(authentication); + String token = jwtTokenProvider.generateJwtToken(authentication); - return new AuthResponse(token, true); - } catch (BadCredentialsException e) { - log.warn("Authentication failed for user with username: {}", email); - throw new UserException("Invalid username or password."); - } catch (Exception e) { - log.error("Unexpected error during login process", e); - throw new UserException("Unexpected error during login process."); - } + return new AuthResponse(token, true); } public static boolean isEmailInvalid(String email) { diff --git a/backend-service/src/main/java/co/teamsphere/api/services/impl/ChatServiceImpl.java b/backend-service/src/main/java/co/teamsphere/api/services/impl/ChatServiceImpl.java index 5e54499..cd8a655 100644 --- a/backend-service/src/main/java/co/teamsphere/api/services/impl/ChatServiceImpl.java +++ b/backend-service/src/main/java/co/teamsphere/api/services/impl/ChatServiceImpl.java @@ -1,11 +1,9 @@ package co.teamsphere.api.services.impl; import co.teamsphere.api.DTO.ChatSummaryDTO; -import co.teamsphere.api.DTOmapper.ChatDTOMapper; import co.teamsphere.api.exception.ChatException; import co.teamsphere.api.exception.UserException; import co.teamsphere.api.models.Chat; -import co.teamsphere.api.models.Messages; import co.teamsphere.api.models.User; import co.teamsphere.api.repository.ChatRepository; import co.teamsphere.api.request.GroupChatRequest; @@ -19,10 +17,10 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.UUID; -import java.util.ArrayList; @Service @Validated @@ -33,12 +31,9 @@ public class ChatServiceImpl implements ChatService { private final ChatRepository chatRepository; - private final ChatDTOMapper chatDTOMapper; - - public ChatServiceImpl(UserService userService, ChatRepository chatRepository, ChatDTOMapper chatDTOMapper) { + public ChatServiceImpl(UserService userService, ChatRepository chatRepository) { this.userService = userService; this.chatRepository = chatRepository; - this.chatDTOMapper = chatDTOMapper; } @Override @@ -147,16 +142,12 @@ public Chat createGroup(GroupChatRequest req, UUID reqUserId) throws UserExcepti } } - //TODO: Add builder pattern here -// chat.builder().chat_name(req.getChat_name()) -//// .chat_image(req.getChat_image()) -//// .is_group(true) -//// .admins(reqUser -//// .build(); - chat.setChatName(req.getChat_name()); - chat.setChatImage(req.getChat_image()); - chat.setIsGroup(true); - chat.getAdmins().add(reqUser); + Chat.builder() + .chatName(req.getChat_name()) + .chatImage(req.getChat_image()) + .isGroup(true) + .admins(Collections.singleton(reqUser)) + .build(); Chat createdChat = chatRepository.save(chat); @@ -246,46 +237,11 @@ public List getChatSummaries(UUID userId, int page, int size) th log.info("Getting chat summaries for user with ID: {}", userId); Pageable pageable = PageRequest.of(page, size); - Page userChatsPage = chatRepository.findChatsByUserId(userId, pageable); - List userChats = userChatsPage.getContent(); - - List chatSummaries = new ArrayList<>(); - for (Chat chat : userChats) { - String[] chatInfo = { chat.getChatName(), chat.getChatImage() }; - - if (!chat.getIsGroup()) { - // Use a wrapper object or array to hold mutable state - final String[] chatNameImage = { chat.getChatName(), chat.getChatImage() }; - - chat.getUsers().stream() - .filter(user -> !user.getId().equals(userId)) - .findFirst() - .ifPresent(otherUser -> { - chatNameImage[0] = otherUser.getUsername(); - chatNameImage[1] = otherUser.getProfilePicture(); - }); - chatInfo[0] = chatNameImage[0]; - chatInfo[1] = chatNameImage[1]; - } - Messages lastMessage = null; - if (!chat.getMessages().isEmpty()) { - lastMessage = chat.getMessages().get(chat.getMessages().size() - 1); - } - - ChatSummaryDTO summary = ChatSummaryDTO.builder() - .id(chat.getId()) - .chatName(chatInfo[0]) - .chatImage(chatInfo[1]) - .createdBy(chat.getCreatedBy().getId()) - .lastMessage(lastMessage != null ? chatDTOMapper.toMessageDto(lastMessage) : null) - .build(); - - chatSummaries.add(summary); - } + Page userChatsPage = chatRepository.findChatsByUserId(userId, pageable); - log.info("Retrieved {} chat summaries for user with ID: {}", chatSummaries.size(), userId); + log.info("Retrieved {} chat summaries for user with ID: {}", userChatsPage.getSize(), userId); - return chatSummaries; + return userChatsPage.getContent(); } catch (Exception e) { log.error("Error getting chat summaries for user with ID: {}", userId, e); throw new ChatException("Error getting chat summaries for user with ID: " + userId + ". " + e.getMessage()); diff --git a/backend-service/src/main/java/co/teamsphere/api/services/impl/CloudflareApiServiceImpl.java b/backend-service/src/main/java/co/teamsphere/api/services/impl/CloudflareApiServiceImpl.java index 5665843..5662812 100644 --- a/backend-service/src/main/java/co/teamsphere/api/services/impl/CloudflareApiServiceImpl.java +++ b/backend-service/src/main/java/co/teamsphere/api/services/impl/CloudflareApiServiceImpl.java @@ -1,10 +1,11 @@ package co.teamsphere.api.services.impl; -import java.io.IOException; -import java.util.List; -import java.util.Objects; -import java.util.UUID; - +import co.teamsphere.api.exception.CloudflareException; +import co.teamsphere.api.response.CloudflareApiResponse; +import co.teamsphere.api.services.CloudflareApiService; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ByteArrayResource; import org.springframework.http.HttpEntity; @@ -16,13 +17,13 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import org.springframework.web.multipart.MultipartFile; -import co.teamsphere.api.response.CloudflareApiResponse; -import co.teamsphere.api.services.CloudflareApiService; - -import lombok.extern.slf4j.Slf4j; +import java.io.IOException; +import java.util.Collections; +import java.util.UUID; @Service @Slf4j @@ -45,38 +46,54 @@ public CloudflareApiResponse uploadImage(MultipartFile imageFile) throws IOExcep headers.set("Authorization", String.format("Bearer %s", cloudflareApiKey)); var body = new LinkedMultiValueMap(); + final String filename = UUID.randomUUID().toString(); body.add("file", new ByteArrayResource(imageFile.getBytes()) { @Override public String getFilename() { - return UUID.randomUUID().toString(); + return filename; } }); body.add("requireSignedURLs", false); - HttpEntity> requestEntity = new HttpEntity<>(body, headers); - String url = apiUrl.replace("{account_id}", cloudflareAccountID); - - CloudflareApiResponse cloudflareApiResponse = new CloudflareApiResponse(); - ResponseEntity response = null; + ResponseEntity response; try { - log.info("Sending ImageID: {} request to URL: {}", body.get("file"), url); + log.info("Sending ImageID: {} request to URL: {}",filename , url); response = restTemplate.postForEntity(url, requestEntity, CloudflareApiResponse.class); + if (response.getStatusCode() == HttpStatus.OK) { - log.info("{} uploaded successfully to Cloudflare.", body.get("file")); + log.info("{} uploaded successfully to Cloudflare.", filename); return response.getBody(); } + log.error("Failed to upload image to Cloudflare. Status code: {}", response.getStatusCode()); - } catch (HttpClientErrorException e){ - assert response != null; - cloudflareApiResponse.setErrors(Objects.requireNonNull(response.getBody()).getErrors()); - log.error("Cloudflare API error: {}", e.getResponseBodyAsString()); - } + return createErrorResponse("Upload failed with status: " + response.getStatusCode()); - return cloudflareApiResponse; + } catch (HttpClientErrorException e) { + log.error("Cloudflare API client error: {}", e.getResponseBodyAsString()); + + try { + // Parse the error response into our API response object + ObjectMapper mapper = new ObjectMapper(); + return mapper.readValue(e.getResponseBodyAsString(), CloudflareApiResponse.class); + } catch (JsonProcessingException jsonException) { + log.error("Failed to parse Cloudflare error response", jsonException); + return createErrorResponse("Authentication failed: " + e.getStatusCode()); + } + + } catch (RestClientException e) { + // Handle other REST client exceptions (network issues, etc.) + log.error("REST client error while uploading to Cloudflare", e); + return createErrorResponse("Failed to communicate with Cloudflare: " + e.getMessage()); + + } catch (Exception e) { + // Catch any other unexpected exceptions + log.error("Unexpected error while uploading to Cloudflare", e); + return createErrorResponse("Internal server error"); + } } @Override @@ -100,16 +117,24 @@ public CloudflareApiResponse deleteImage(String imageID) { ); if (response.getStatusCode() == HttpStatus.OK) { - log.info("ImageID: {} deleted successfully from Cloudflare."); + log.info("ImageID: {} deleted successfully from Cloudflare.", imageID); return response.getBody(); } log.error("Failed to delete image from Cloudflare. Status code: {}", response.getStatusCode()); } catch (HttpClientErrorException e) { log.error("Cloudflare API error: {}", e.getResponseBodyAsString()); - cloudflareApiResponse.setErrors(List.of(e.getMessage())); + createErrorResponse(e.getMessage()); } - return cloudflareApiResponse; } + + private CloudflareApiResponse createErrorResponse(String message) { + CloudflareApiResponse errorResponse = new CloudflareApiResponse(); + errorResponse.setSuccess(false); + CloudflareException error = new CloudflareException(); + error.setMessage(message); + errorResponse.setErrors(Collections.singletonList(error)); + return errorResponse; + } } \ No newline at end of file diff --git a/backend-service/src/main/resources/application-local.yml b/backend-service/src/main/resources/application-local.yml index 2d84a75..192f711 100644 --- a/backend-service/src/main/resources/application-local.yml +++ b/backend-service/src/main/resources/application-local.yml @@ -17,7 +17,7 @@ spring: jpa: database: mysql hibernate: - ddl-auto: create-drop + ddl-auto: update show-sql: true rabbitmq: host: localhost diff --git a/pom.xml b/pom.xml index 45815ce..f3ab1e9 100644 --- a/pom.xml +++ b/pom.xml @@ -103,6 +103,16 @@ jackson-annotations 2.17.1 + + com.fasterxml.jackson.core + jackson-databind + 2.17.1 + + + com.fasterxml.jackson.core + jackson-core + 2.17.1 + org.springframework.cloud spring-cloud-dependencies