From a8fdb16ee13fcbd13f80fa30d50d06b15bc2504a Mon Sep 17 00:00:00 2001 From: adikatre Date: Wed, 11 Feb 2026 11:13:39 -0800 Subject: [PATCH 1/6] security permitall chat(ONLY FOR TESTING!) Co-authored-by: Nikhil Maturi --- src/main/java/com/open/spring/security/SecurityConfig.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/open/spring/security/SecurityConfig.java b/src/main/java/com/open/spring/security/SecurityConfig.java index c0b5b20a..5c8ed4d4 100644 --- a/src/main/java/com/open/spring/security/SecurityConfig.java +++ b/src/main/java/com/open/spring/security/SecurityConfig.java @@ -11,8 +11,8 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; /* @@ -133,6 +133,9 @@ public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exce .requestMatchers("/api/analytics/**").permitAll() .requestMatchers("/api/plant/**").permitAll() .requestMatchers("/api/groups/**").permitAll() + .requestMatchers("/api/files/**").permitAll() + // Chat APIs - require authentication + .requestMatchers("/api/chat/**").permitAll() .requestMatchers("/api/grade-prediction/**").permitAll() .requestMatchers("/api/admin-evaluation/**").permitAll() .requestMatchers("/api/grades/**").permitAll() From 6c5437ed805596fa64db0e2951764b7e21c0e7f7 Mon Sep 17 00:00:00 2001 From: adikatre Date: Wed, 11 Feb 2026 11:14:24 -0800 Subject: [PATCH 2/6] chat message for jpa Co-authored-by: Nikhil Maturi --- .../java/com/open/spring/mvc/chat/ChatMessage.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/main/java/com/open/spring/mvc/chat/ChatMessage.java diff --git a/src/main/java/com/open/spring/mvc/chat/ChatMessage.java b/src/main/java/com/open/spring/mvc/chat/ChatMessage.java new file mode 100644 index 00000000..c5068f60 --- /dev/null +++ b/src/main/java/com/open/spring/mvc/chat/ChatMessage.java @@ -0,0 +1,14 @@ +package com.open.spring.mvc.chat; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ChatMessage { + private String sender; + private String content; + private Long timestamp; +} From b45e9d416632d114937041be9ca70b5ce1f3e7e9 Mon Sep 17 00:00:00 2001 From: adikatre Date: Wed, 11 Feb 2026 11:14:41 -0800 Subject: [PATCH 3/6] api controller Co-authored-by: Nikhil Maturi --- .../spring/mvc/chat/ChatApiController.java | 116 ++++++++++++++++++ .../com/open/spring/mvc/chat/ChatService.java | 98 +++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 src/main/java/com/open/spring/mvc/chat/ChatApiController.java create mode 100644 src/main/java/com/open/spring/mvc/chat/ChatService.java diff --git a/src/main/java/com/open/spring/mvc/chat/ChatApiController.java b/src/main/java/com/open/spring/mvc/chat/ChatApiController.java new file mode 100644 index 00000000..d1a6e1f0 --- /dev/null +++ b/src/main/java/com/open/spring/mvc/chat/ChatApiController.java @@ -0,0 +1,116 @@ +package com.open.spring.mvc.chat; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.open.spring.mvc.groups.Groups; +import com.open.spring.mvc.groups.GroupsJpaRepository; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@RestController +@RequestMapping("/api/chat") +@CrossOrigin +public class ChatApiController { + + private final ChatService chatService; + private final GroupsJpaRepository groupsRepository; + + public ChatApiController(ChatService chatService, GroupsJpaRepository groupsRepository) { + this.chatService = chatService; + this.groupsRepository = groupsRepository; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class ChatMessageRequest { + private String content; + } + + @GetMapping("/{groupId}") + @Transactional(readOnly = true) + public ResponseEntity> getChat(@PathVariable Long groupId) { + Optional groupOpt = groupsRepository.findById(groupId); + if (groupOpt.isEmpty()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + // String currentUsername = getCurrentUsername(); + // if (currentUsername == null) { + // return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + // } + + // if (!isMember(groupOpt.get(), currentUsername)) { + // return new ResponseEntity<>(HttpStatus.FORBIDDEN); + // } + + return new ResponseEntity<>(chatService.getChatHistory(groupId), HttpStatus.OK); + } + + @PostMapping("/{groupId}") + @Transactional + public ResponseEntity> postChat( + @PathVariable Long groupId, + @RequestBody ChatMessageRequest request) { + Optional groupOpt = groupsRepository.findById(groupId); + if (groupOpt.isEmpty()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + String currentUsername = getCurrentUsername(); + if (currentUsername == null) { + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } + + if (!isMember(groupOpt.get(), currentUsername)) { + return new ResponseEntity<>(HttpStatus.FORBIDDEN); + } + + if (request == null || request.getContent() == null || request.getContent().isBlank()) { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + ChatMessage message = new ChatMessage( + currentUsername, + request.getContent().trim(), + Instant.now().toEpochMilli() + ); + + List updated = chatService.addMessage(groupId, message); + return new ResponseEntity<>(updated, HttpStatus.OK); + } + + private String getCurrentUsername() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null || !auth.isAuthenticated()) { + return null; + } + String name = auth.getName(); + if (name == null || name.isBlank() || "anonymousUser".equals(name)) { + return null; + } + return name; + } + + private boolean isMember(Groups group, String uid) { + return group.getGroupMembers().stream() + .anyMatch(member -> uid.equals(member.getUid())); + } +} diff --git a/src/main/java/com/open/spring/mvc/chat/ChatService.java b/src/main/java/com/open/spring/mvc/chat/ChatService.java new file mode 100644 index 00000000..ac9c14d8 --- /dev/null +++ b/src/main/java/com/open/spring/mvc/chat/ChatService.java @@ -0,0 +1,98 @@ +package com.open.spring.mvc.chat; + +import java.io.BufferedReader; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.open.spring.mvc.S3uploads.S3FileHandler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ChatService { + private static final String CHAT_FILENAME = "chat_history.jsonl"; + + private final S3FileHandler s3FileHandler; + private final ObjectMapper objectMapper; + + public List getChatHistory(Long groupId) { + return readHistory(groupId.toString()); + } + + public List addMessage(Long groupId, ChatMessage message) { + List history = readHistory(groupId.toString()); + history.add(message); + writeHistory(groupId.toString(), history); + return history; + } + + private List readHistory(String groupId) { + String base64Data = s3FileHandler.decodeFile(groupId, CHAT_FILENAME); + if (base64Data == null || base64Data.isBlank()) { + return new ArrayList<>(); + } + + byte[] decoded; + try { + decoded = Base64.getDecoder().decode(base64Data); + } catch (IllegalArgumentException e) { + log.warn("Invalid base64 data for group {} chat history.", groupId, e); + return new ArrayList<>(); + } + + String jsonl = new String(decoded, StandardCharsets.UTF_8); + if (jsonl.isBlank()) { + return new ArrayList<>(); + } + + List messages = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader(new StringReader(jsonl))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.isBlank()) { + continue; + } + try { + ChatMessage message = objectMapper.readValue(line, ChatMessage.class); + messages.add(message); + } catch (Exception e) { + log.warn("Skipping invalid chat line for group {}: {}", groupId, line, e); + } + } + } catch (Exception e) { + log.warn("Failed reading chat history for group {}", groupId, e); + } + + return messages; + } + + private void writeHistory(String groupId, List history) { + String jsonl = history.stream() + .map(this::toJson) + .collect(Collectors.joining("\n")); + + String base64Data = Base64.getEncoder() + .encodeToString(jsonl.getBytes(StandardCharsets.UTF_8)); + + s3FileHandler.uploadFile(base64Data, CHAT_FILENAME, groupId); + } + + private String toJson(ChatMessage message) { + try { + return objectMapper.writeValueAsString(message); + } catch (Exception e) { + log.warn("Failed to serialize chat message: {}", message, e); + return "{}"; + } + } +} From d1bbcaecd1b8743935b7b4586b897348be259045 Mon Sep 17 00:00:00 2001 From: adikatre Date: Wed, 11 Feb 2026 11:18:30 -0800 Subject: [PATCH 4/6] Finalize iteration of group chat Co-authored-by: Nikhil Maturi --- .../spring/mvc/S3uploads/FileHandler.java | 10 +- .../spring/mvc/S3uploads/S3FileHandler.java | 25 +++ .../mvc/groups/GroupChatApiController.java | 162 ++++++++++++++++++ .../spring/mvc/groups/GroupChatMessage.java | 15 ++ .../spring/mvc/groups/GroupChatService.java | 108 ++++++++++++ 5 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/open/spring/mvc/groups/GroupChatApiController.java create mode 100644 src/main/java/com/open/spring/mvc/groups/GroupChatMessage.java create mode 100644 src/main/java/com/open/spring/mvc/groups/GroupChatService.java diff --git a/src/main/java/com/open/spring/mvc/S3uploads/FileHandler.java b/src/main/java/com/open/spring/mvc/S3uploads/FileHandler.java index d18865d8..df3ce990 100644 --- a/src/main/java/com/open/spring/mvc/S3uploads/FileHandler.java +++ b/src/main/java/com/open/spring/mvc/S3uploads/FileHandler.java @@ -22,9 +22,17 @@ public interface FileHandler { /** * Deletes all files associated with a user. - * + * * @param uid User ID * @return true if successful */ boolean deleteFiles(String uid); + + /** + * Lists all file keys under a given prefix. + * + * @param prefix The S3 key prefix to list + * @return List of S3 object keys matching the prefix + */ + java.util.List listFiles(String prefix); } diff --git a/src/main/java/com/open/spring/mvc/S3uploads/S3FileHandler.java b/src/main/java/com/open/spring/mvc/S3uploads/S3FileHandler.java index 953f4b3c..604e90c5 100644 --- a/src/main/java/com/open/spring/mvc/S3uploads/S3FileHandler.java +++ b/src/main/java/com/open/spring/mvc/S3uploads/S3FileHandler.java @@ -1,5 +1,6 @@ package com.open.spring.mvc.S3uploads; +import java.util.ArrayList; import java.util.Base64; import java.util.List; import java.util.stream.Collectors; @@ -153,6 +154,30 @@ public boolean deleteFiles(String uid) { } } + @Override + public List listFiles(String prefix) { + if (s3Client == null) { + log.warn("S3 list attempted but S3 client is not configured."); + return new ArrayList<>(); + } + + try { + ListObjectsV2Request listReq = ListObjectsV2Request.builder() + .bucket(bucketName) + .prefix(prefix) + .build(); + + ListObjectsV2Response listRes = s3Client.listObjectsV2(listReq); + + return listRes.contents().stream() + .map(s3Object -> s3Object.key()) + .collect(Collectors.toList()); + } catch (Exception e) { + e.printStackTrace(); + return new ArrayList<>(); + } + } + private String generateKey(String uid, String filename) { return uid + "/" + filename; } diff --git a/src/main/java/com/open/spring/mvc/groups/GroupChatApiController.java b/src/main/java/com/open/spring/mvc/groups/GroupChatApiController.java new file mode 100644 index 00000000..70e530ed --- /dev/null +++ b/src/main/java/com/open/spring/mvc/groups/GroupChatApiController.java @@ -0,0 +1,162 @@ +package com.open.spring.mvc.groups; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +// import org.springframework.security.core.Authentication; +// import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@RestController +@RequestMapping("/api/groups/chat") +@CrossOrigin +public class GroupChatApiController { + + private final GroupChatService groupChatService; + private final GroupsJpaRepository groupsRepository; + + public GroupChatApiController(GroupChatService groupChatService, GroupsJpaRepository groupsRepository) { + this.groupChatService = groupChatService; + this.groupsRepository = groupsRepository; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class FileUploadRequest { + private String filename; + private String base64Data; + } + + // --- Auth helpers (commented out) --- + // private String getCurrentUsername() { + // Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + // if (auth == null || !auth.isAuthenticated()) { + // return null; + // } + // String name = auth.getName(); + // if (name == null || name.isBlank() || "anonymousUser".equals(name)) { + // return null; + // } + // return name; + // } + // + // private boolean isMember(Groups group, String uid) { + // return group.getGroupMembers().stream() + // .anyMatch(member -> uid.equals(member.getUid())); + // } + + @GetMapping("/{groupId}/messages") + public ResponseEntity getMessages(@PathVariable Long groupId) { + Optional groupOpt = groupsRepository.findById(groupId); + if (groupOpt.isEmpty()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + // String currentUsername = getCurrentUsername(); + // if (currentUsername == null) { + // return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + // } + // if (!isMember(groupOpt.get(), currentUsername)) { + // return new ResponseEntity<>(HttpStatus.FORBIDDEN); + // } + + String groupName = groupOpt.get().getName(); + List messages = groupChatService.getMessages(groupName); + return new ResponseEntity<>(messages, HttpStatus.OK); + } + + @PostMapping("/{groupId}/messages") + public ResponseEntity postMessage( + @PathVariable Long groupId, + @RequestBody GroupChatMessage message) { + Optional groupOpt = groupsRepository.findById(groupId); + if (groupOpt.isEmpty()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + // String currentUsername = getCurrentUsername(); + // if (currentUsername == null) { + // return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + // } + // if (!isMember(groupOpt.get(), currentUsername)) { + // return new ResponseEntity<>(HttpStatus.FORBIDDEN); + // } + + if (message == null || message.getName() == null || message.getMessage() == null) { + return new ResponseEntity<>("name and message are required", HttpStatus.BAD_REQUEST); + } + + String groupName = groupOpt.get().getName(); + List updated = groupChatService.addMessage(groupName, message); + return new ResponseEntity<>(updated, HttpStatus.OK); + } + + @GetMapping("/{groupId}/files") + public ResponseEntity getSharedFiles(@PathVariable Long groupId) { + Optional groupOpt = groupsRepository.findById(groupId); + if (groupOpt.isEmpty()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + // String currentUsername = getCurrentUsername(); + // if (currentUsername == null) { + // return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + // } + // if (!isMember(groupOpt.get(), currentUsername)) { + // return new ResponseEntity<>(HttpStatus.FORBIDDEN); + // } + + String groupName = groupOpt.get().getName(); + List files = groupChatService.listSharedFiles(groupName); + return new ResponseEntity<>(files, HttpStatus.OK); + } + + @PostMapping("/{groupId}/files") + public ResponseEntity uploadSharedFile( + @PathVariable Long groupId, + @RequestBody FileUploadRequest request) { + Optional groupOpt = groupsRepository.findById(groupId); + if (groupOpt.isEmpty()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + // String currentUsername = getCurrentUsername(); + // if (currentUsername == null) { + // return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + // } + // if (!isMember(groupOpt.get(), currentUsername)) { + // return new ResponseEntity<>(HttpStatus.FORBIDDEN); + // } + + if (request == null || request.getFilename() == null || request.getBase64Data() == null) { + return new ResponseEntity<>("filename and base64Data are required", HttpStatus.BAD_REQUEST); + } + + String groupName = groupOpt.get().getName(); + String result = groupChatService.uploadSharedFile(groupName, request.getFilename(), request.getBase64Data()); + + if (result != null) { + Map response = new HashMap<>(); + response.put("message", "File uploaded successfully"); + response.put("filename", request.getFilename()); + return new ResponseEntity<>(response, HttpStatus.OK); + } else { + return new ResponseEntity<>("Upload failed", HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/com/open/spring/mvc/groups/GroupChatMessage.java b/src/main/java/com/open/spring/mvc/groups/GroupChatMessage.java new file mode 100644 index 00000000..81d69fab --- /dev/null +++ b/src/main/java/com/open/spring/mvc/groups/GroupChatMessage.java @@ -0,0 +1,15 @@ +package com.open.spring.mvc.groups; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class GroupChatMessage { + private String name; + private String message; + private String date; + private String image; +} diff --git a/src/main/java/com/open/spring/mvc/groups/GroupChatService.java b/src/main/java/com/open/spring/mvc/groups/GroupChatService.java new file mode 100644 index 00000000..b1a2464b --- /dev/null +++ b/src/main/java/com/open/spring/mvc/groups/GroupChatService.java @@ -0,0 +1,108 @@ +package com.open.spring.mvc.groups; + +import java.io.BufferedReader; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.open.spring.mvc.S3uploads.S3FileHandler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@RequiredArgsConstructor +@Slf4j +public class GroupChatService { + private static final String MESSAGES_FILE = "messages-images/messages.jsonl"; + private static final String SHARED_FILES_PREFIX = "shared-files/"; + + private final S3FileHandler s3FileHandler; + private final ObjectMapper objectMapper; + + public List getMessages(String groupName) { + String base64Data = s3FileHandler.decodeFile(groupName, MESSAGES_FILE); + if (base64Data == null || base64Data.isBlank()) { + return new ArrayList<>(); + } + + byte[] decoded; + try { + decoded = Base64.getDecoder().decode(base64Data); + } catch (IllegalArgumentException e) { + log.warn("Invalid base64 data for group {} messages.", groupName, e); + return new ArrayList<>(); + } + + String jsonl = new String(decoded, StandardCharsets.UTF_8); + if (jsonl.isBlank()) { + return new ArrayList<>(); + } + + List messages = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader(new StringReader(jsonl))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.isBlank()) continue; + try { + GroupChatMessage msg = objectMapper.readValue(line, GroupChatMessage.class); + messages.add(msg); + } catch (Exception e) { + log.warn("Skipping invalid message line for group {}: {}", groupName, line, e); + } + } + } catch (Exception e) { + log.warn("Failed reading messages for group {}", groupName, e); + } + + return messages; + } + + public List addMessage(String groupName, GroupChatMessage message) { + List messages = getMessages(groupName); + messages.add(message); + + String jsonl = messages.stream() + .map(this::toJson) + .collect(Collectors.joining("\n")); + + String base64Data = Base64.getEncoder() + .encodeToString(jsonl.getBytes(StandardCharsets.UTF_8)); + + s3FileHandler.uploadFile(base64Data, MESSAGES_FILE, groupName); + return messages; + } + + public List listSharedFiles(String groupName) { + String prefix = groupName + "/" + SHARED_FILES_PREFIX; + List keys = s3FileHandler.listFiles(prefix); + + return keys.stream() + .map(key -> key.substring(prefix.length())) + .filter(name -> !name.isEmpty()) + .collect(Collectors.toList()); + } + + public String uploadSharedFile(String groupName, String filename, String base64Data) { + return s3FileHandler.uploadFile(base64Data, SHARED_FILES_PREFIX + filename, groupName); + } + + public String downloadSharedFile(String groupName, String filename) { + return s3FileHandler.decodeFile(groupName, SHARED_FILES_PREFIX + filename); + } + + private String toJson(GroupChatMessage message) { + try { + return objectMapper.writeValueAsString(message); + } catch (Exception e) { + log.warn("Failed to serialize message: {}", message, e); + return "{}"; + } + } +} From 4ebda098d081d539e35e0d5c97575a4da92d3bd2 Mon Sep 17 00:00:00 2001 From: adikatre Date: Wed, 11 Feb 2026 11:30:19 -0800 Subject: [PATCH 5/6] Prevent group name duplication Co-authored-by: Nikhil Maturi --- .../mvc/groups/GroupsApiController.java | 21 +++++++++++++++++++ .../mvc/groups/GroupsJpaRepository.java | 3 +++ 2 files changed, 24 insertions(+) diff --git a/src/main/java/com/open/spring/mvc/groups/GroupsApiController.java b/src/main/java/com/open/spring/mvc/groups/GroupsApiController.java index 2637b7fb..373b4dd9 100644 --- a/src/main/java/com/open/spring/mvc/groups/GroupsApiController.java +++ b/src/main/java/com/open/spring/mvc/groups/GroupsApiController.java @@ -36,6 +36,9 @@ public class GroupsApiController { @Autowired private PersonJpaRepository personRepository; + @Autowired + private GroupChatService groupChatService; + // ===== DTOs ===== @Data @NoArgsConstructor @@ -197,6 +200,13 @@ public ResponseEntity> createGroup(@RequestBody GroupCreateD ); } + if (groupsRepository.findByName(dto.getName()).isPresent()) { + return new ResponseEntity<>( + Map.of("error", "Group with name '" + dto.getName() + "' already exists"), + HttpStatus.CONFLICT + ); + } + Groups group = new Groups(); group.setName(dto.getName()); group.setPeriod(dto.getPeriod()); @@ -215,6 +225,9 @@ public ResponseEntity> createGroup(@RequestBody GroupCreateD savedGroup = groupsRepository.save(savedGroup); } + // Initialize S3 folder structure for the new group + groupChatService.initGroupStorage(savedGroup.getName()); + return new ResponseEntity<>(buildGroupResponse(savedGroup), HttpStatus.CREATED); } catch (Exception e) { return new ResponseEntity<>( @@ -245,6 +258,11 @@ public ResponseEntity> bulkCreateGroups(@RequestBody BulkGro for (GroupCreateDto groupDto : dto.getGroups()) { try { + if (groupsRepository.findByName(groupDto.getName()).isPresent()) { + errors.add("Group with name '" + groupDto.getName() + "' already exists"); + continue; + } + Groups group = new Groups(); group.setName(groupDto.getName()); group.setPeriod(groupDto.getPeriod()); @@ -262,6 +280,9 @@ public ResponseEntity> bulkCreateGroups(@RequestBody BulkGro savedGroup = groupsRepository.save(savedGroup); } + // Initialize S3 folder structure for the new group + groupChatService.initGroupStorage(savedGroup.getName()); + created.add(buildGroupResponse(savedGroup)); } catch (Exception e) { errors.add("Failed to create group '" + groupDto.getName() + "': " + e.getMessage()); diff --git a/src/main/java/com/open/spring/mvc/groups/GroupsJpaRepository.java b/src/main/java/com/open/spring/mvc/groups/GroupsJpaRepository.java index 8ce48a95..7763c659 100644 --- a/src/main/java/com/open/spring/mvc/groups/GroupsJpaRepository.java +++ b/src/main/java/com/open/spring/mvc/groups/GroupsJpaRepository.java @@ -42,6 +42,9 @@ public interface GroupsJpaRepository extends JpaRepository { "ORDER BY p.id", nativeQuery = true) List findGroupMembersRaw(@Param("groupId") Long groupId); + // Find a group by exact name + Optional findByName(String name); + // Search groups by name (case-insensitive, partial match) @Query("SELECT g FROM Groups g WHERE LOWER(g.name) LIKE LOWER(CONCAT('%', :searchTerm, '%')) ORDER BY g.name") List searchByName(@Param("searchTerm") String searchTerm); From af330eb369448057cf064728746d817161938221 Mon Sep 17 00:00:00 2001 From: adikatre Date: Wed, 11 Feb 2026 11:30:37 -0800 Subject: [PATCH 6/6] groups creates folders on initial creation Co-authored-by: Nikhil Maturi --- .../mvc/groups/GroupChatApiController.java | 2 +- .../spring/mvc/groups/GroupChatService.java | 25 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/open/spring/mvc/groups/GroupChatApiController.java b/src/main/java/com/open/spring/mvc/groups/GroupChatApiController.java index 70e530ed..027918e2 100644 --- a/src/main/java/com/open/spring/mvc/groups/GroupChatApiController.java +++ b/src/main/java/com/open/spring/mvc/groups/GroupChatApiController.java @@ -122,7 +122,7 @@ public ResponseEntity getSharedFiles(@PathVariable Long groupId) { // } String groupName = groupOpt.get().getName(); - List files = groupChatService.listSharedFiles(groupName); + List> files = groupChatService.listSharedFiles(groupName); return new ResponseEntity<>(files, HttpStatus.OK); } diff --git a/src/main/java/com/open/spring/mvc/groups/GroupChatService.java b/src/main/java/com/open/spring/mvc/groups/GroupChatService.java index b1a2464b..a2cbba9a 100644 --- a/src/main/java/com/open/spring/mvc/groups/GroupChatService.java +++ b/src/main/java/com/open/spring/mvc/groups/GroupChatService.java @@ -5,7 +5,9 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Base64; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import org.springframework.stereotype.Service; @@ -26,6 +28,15 @@ public class GroupChatService { private final S3FileHandler s3FileHandler; private final ObjectMapper objectMapper; + public void initGroupStorage(String groupName) { + // Create empty messages.jsonl file + String emptyBase64 = Base64.getEncoder().encodeToString(new byte[0]); + s3FileHandler.uploadFile(emptyBase64, MESSAGES_FILE, groupName); + + // Create shared-files/ folder (S3 uses a zero-byte placeholder to represent an empty folder) + s3FileHandler.uploadFile(emptyBase64, SHARED_FILES_PREFIX, groupName); + } + public List getMessages(String groupName) { String base64Data = s3FileHandler.decodeFile(groupName, MESSAGES_FILE); if (base64Data == null || base64Data.isBlank()) { @@ -79,14 +90,24 @@ public List addMessage(String groupName, GroupChatMessage mess return messages; } - public List listSharedFiles(String groupName) { + public List> listSharedFiles(String groupName) { String prefix = groupName + "/" + SHARED_FILES_PREFIX; List keys = s3FileHandler.listFiles(prefix); - return keys.stream() + List filenames = keys.stream() .map(key -> key.substring(prefix.length())) .filter(name -> !name.isEmpty()) .collect(Collectors.toList()); + + List> files = new ArrayList<>(); + for (String filename : filenames) { + Map fileEntry = new HashMap<>(); + fileEntry.put("filename", filename); + String base64Data = s3FileHandler.decodeFile(groupName, SHARED_FILES_PREFIX + filename); + fileEntry.put("base64Data", base64Data); + files.add(fileEntry); + } + return files; } public String uploadSharedFile(String groupName, String filename, String base64Data) {