Skip to content

Commit 77b27bf

Browse files
committed
feat(forum): add quick filter functionality and public access endpoints
- Introduced new public endpoints for forum quick filters, allowing users to retrieve filter options for post listings. - Added a new DTO for quick filter options to standardize data representation. - Updated the ForumController to handle requests for quick filters and integrated it with the ForumService. - Enhanced SecurityConfig to allow public access to forum-related endpoints. - Refactored existing comment and post handling to include author information, improving data consistency.
1 parent e59f23e commit 77b27bf

22 files changed

Lines changed: 687 additions & 124 deletions

File tree

backend-spring/src/main/java/com/ulticode/common/config/SecurityConfig.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ public class SecurityConfig {
4646
// Problem endpoints (public read access)
4747
"/problems",
4848
"/problems/**",
49+
// Forum endpoints (public read access)
50+
"/forum/posts",
51+
"/forum/posts/**",
52+
"/forum/communities",
53+
"/forum/communities/**",
54+
"/forum/tags",
55+
"/forum/quick-filters",
4956
// Swagger/OpenAPI documentation
5057
"/swagger-ui/**",
5158
"/swagger-ui.html",

backend-spring/src/main/java/com/ulticode/modules/forum/controller/ForumController.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,24 @@ public Result<List<ForumTagVO>> getAllTags() {
368368
return Result.success(tags);
369369
}
370370

371+
// =========================================================================
372+
// QUICK FILTER OPERATIONS (Public)
373+
// =========================================================================
374+
375+
/**
376+
* Get quick filter options.
377+
* Public endpoint - accessible without authentication.
378+
* Returns available filter options for post listings (e.g., hot, new, top).
379+
*
380+
* @return list of quick filters
381+
*/
382+
@Operation(summary = "Get quick filters", description = "Get available quick filter options for forum posts")
383+
@GetMapping("/quick-filters")
384+
public Result<List<QuickFilterDTO>> getQuickFilters() {
385+
List<QuickFilterDTO> filters = forumService.getQuickFilters();
386+
return Result.success(filters);
387+
}
388+
371389
// =========================================================================
372390
// HELPER METHODS
373391
// =========================================================================
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.ulticode.modules.forum.dto;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
8+
/**
9+
* DTO for forum quick filter options.
10+
* Represents a filter that users can apply to post listings.
11+
*/
12+
@Data
13+
@NoArgsConstructor
14+
@AllArgsConstructor
15+
@Schema(description = "Quick filter option for forum posts")
16+
public class QuickFilterDTO {
17+
18+
@Schema(description = "Display label for the filter (will be translated on frontend)")
19+
private String label;
20+
21+
@Schema(description = "Filter value identifier (e.g., 'hot', 'new', 'top')")
22+
private String value;
23+
}

backend-spring/src/main/java/com/ulticode/modules/forum/service/ForumService.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,16 @@ public interface ForumService {
182182
* @return list of tags
183183
*/
184184
List<ForumTagVO> findAllTags();
185+
186+
// =========================================================================
187+
// QUICK FILTER OPERATIONS
188+
// =========================================================================
189+
190+
/**
191+
* Get all quick filter options.
192+
* Returns available filter options for post listings.
193+
*
194+
* @return list of quick filters
195+
*/
196+
List<QuickFilterDTO> getQuickFilters();
185197
}

backend-spring/src/main/java/com/ulticode/modules/forum/service/impl/ForumServiceImpl.java

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66
import com.ulticode.modules.forum.entity.*;
77
import com.ulticode.modules.forum.mapper.*;
88
import com.ulticode.modules.forum.service.ForumService;
9+
import com.ulticode.modules.user.entity.User;
10+
import com.ulticode.modules.user.service.UserService;
911
import lombok.RequiredArgsConstructor;
1012
import lombok.extern.slf4j.Slf4j;
1113
import org.springframework.stereotype.Service;
1214
import org.springframework.transaction.annotation.Transactional;
1315

1416
import java.time.LocalDateTime;
15-
import java.util.Collections;
16-
import java.util.List;
17-
import java.util.UUID;
17+
import java.util.*;
1818
import java.util.stream.Collectors;
1919

2020
/**
@@ -33,6 +33,7 @@ public class ForumServiceImpl implements ForumService {
3333
private final ForumCommunityMapper communityMapper;
3434
private final ForumCommunityMemberMapper memberMapper;
3535
private final ForumTagMapper tagMapper;
36+
private final UserService userService;
3637

3738
// =========================================================================
3839
// POST OPERATIONS
@@ -196,11 +197,22 @@ public ForumPostThreadVO getPostThread(String postId, String userId) {
196197
// Get all comments for the post
197198
List<ForumComment> comments = commentMapper.findByPostId(postId);
198199

199-
// Build comment tree
200-
List<ForumCommentVO> commentVOs = buildCommentTree(comments);
200+
// Batch fetch all authors to avoid N+1 queries (including post author)
201+
Set<String> authorIds = comments.stream()
202+
.map(ForumComment::getAuthorId)
203+
.collect(Collectors.toSet());
204+
authorIds.add(post.getUserId()); // Include post author
205+
206+
Map<String, User> authorMap = new HashMap<>();
207+
for (String authorId : authorIds) {
208+
userService.findById(authorId).ifPresent(user -> authorMap.put(authorId, user));
209+
}
210+
211+
// Build comment tree with author info
212+
List<ForumCommentVO> commentVOs = buildCommentTree(comments, authorMap);
201213

202214
ForumPostThreadVO thread = new ForumPostThreadVO();
203-
thread.setPost(convertToPostVO(post, userId));
215+
thread.setPost(convertToPostVO(post, userId, authorMap.get(post.getUserId())));
204216
thread.setComments(commentVOs);
205217

206218
return thread;
@@ -254,7 +266,11 @@ public ForumCommentVO createComment(String postId, CreateCommentDTO dto, String
254266

255267
commentMapper.insert(comment);
256268

257-
return convertToCommentVO(comment);
269+
// Fetch author info for the response
270+
Map<String, User> authorMap = new HashMap<>();
271+
userService.findById(userId).ifPresent(user -> authorMap.put(userId, user));
272+
273+
return convertToCommentVO(comment, authorMap);
258274
}
259275

260276
@Override
@@ -277,7 +293,11 @@ public ForumCommentVO updateComment(String id, UpdateCommentDTO dto, String user
277293
commentMapper.updateById(comment);
278294
commentMapper.markAsEdited(id);
279295

280-
return convertToCommentVO(comment);
296+
// Fetch author info for the response
297+
Map<String, User> authorMap = new HashMap<>();
298+
userService.findById(comment.getAuthorId()).ifPresent(user -> authorMap.put(comment.getAuthorId(), user));
299+
300+
return convertToCommentVO(comment, authorMap);
281301
}
282302

283303
@Override
@@ -415,11 +435,31 @@ public List<ForumTagVO> findAllTags() {
415435
.collect(Collectors.toList());
416436
}
417437

438+
// =========================================================================
439+
// QUICK FILTER OPERATIONS
440+
// =========================================================================
441+
442+
@Override
443+
public List<QuickFilterDTO> getQuickFilters() {
444+
log.debug("Getting quick filters");
445+
// Returns the available filter options for forum posts
446+
// The label will be translated on the frontend using i18n
447+
return List.of(
448+
new QuickFilterDTO("Hot", "hot"),
449+
new QuickFilterDTO("New", "new"),
450+
new QuickFilterDTO("Top", "top")
451+
);
452+
}
453+
418454
// =========================================================================
419455
// HELPER METHODS
420456
// =========================================================================
421457

422458
private ForumPostVO convertToPostVO(ForumPost post, String userId) {
459+
return convertToPostVO(post, userId, null);
460+
}
461+
462+
private ForumPostVO convertToPostVO(ForumPost post, String userId, User author) {
423463
ForumPostVO vo = new ForumPostVO();
424464
vo.setId(post.getId());
425465
vo.setCommunityId(post.getCommunityId());
@@ -450,6 +490,12 @@ private ForumPostVO convertToPostVO(ForumPost post, String userId) {
450490
vo.setFlaggedAt(post.getFlaggedAt());
451491
vo.setCreatedAt(post.getCreatedAt());
452492

493+
// Populate author info if available
494+
if (author != null) {
495+
vo.setAuthorUsername(author.getUsername());
496+
vo.setAuthorAvatar(author.getAvatar());
497+
}
498+
453499
// Check if user is member of community (if userId provided)
454500
if (userId != null) {
455501
vo.setIsMember(memberMapper.isMember(post.getCommunityId(), userId));
@@ -458,12 +504,20 @@ private ForumPostVO convertToPostVO(ForumPost post, String userId) {
458504
return vo;
459505
}
460506

461-
private ForumCommentVO convertToCommentVO(ForumComment comment) {
507+
private ForumCommentVO convertToCommentVO(ForumComment comment, Map<String, User> authorMap) {
462508
ForumCommentVO vo = new ForumCommentVO();
463509
vo.setId(comment.getId());
464510
vo.setPostId(comment.getPostId());
465511
vo.setParentId(comment.getParentId());
466512
vo.setAuthorId(comment.getAuthorId());
513+
514+
// Populate author info from author map
515+
User author = authorMap.get(comment.getAuthorId());
516+
if (author != null) {
517+
vo.setAuthorUsername(author.getUsername());
518+
vo.setAuthorAvatar(author.getAvatar());
519+
}
520+
467521
vo.setBody(comment.getBody());
468522
vo.setMarkdown(comment.getMarkdown());
469523
vo.setCreatedAt(comment.getCreatedAt());
@@ -510,17 +564,17 @@ private ForumTagVO convertToTagVO(ForumTag tag) {
510564
return vo;
511565
}
512566

513-
private List<ForumCommentVO> buildCommentTree(List<ForumComment> comments) {
567+
private List<ForumCommentVO> buildCommentTree(List<ForumComment> comments, Map<String, User> authorMap) {
514568
// Separate top-level comments and replies
515569
List<ForumComment> topLevelComments = comments.stream()
516570
.filter(c -> c.getParentId() == null)
517571
.collect(Collectors.toList());
518572

519573
return topLevelComments.stream()
520574
.map(c -> {
521-
ForumCommentVO vo = convertToCommentVO(c);
575+
ForumCommentVO vo = convertToCommentVO(c, authorMap);
522576
// Recursively build replies
523-
List<ForumCommentVO> replies = findReplies(c.getId(), comments);
577+
List<ForumCommentVO> replies = findReplies(c.getId(), comments, authorMap);
524578
if (!replies.isEmpty()) {
525579
vo.setReplies(replies);
526580
}
@@ -529,12 +583,12 @@ private List<ForumCommentVO> buildCommentTree(List<ForumComment> comments) {
529583
.collect(Collectors.toList());
530584
}
531585

532-
private List<ForumCommentVO> findReplies(String parentId, List<ForumComment> allComments) {
586+
private List<ForumCommentVO> findReplies(String parentId, List<ForumComment> allComments, Map<String, User> authorMap) {
533587
return allComments.stream()
534588
.filter(c -> parentId.equals(c.getParentId()))
535589
.map(c -> {
536-
ForumCommentVO vo = convertToCommentVO(c);
537-
vo.setReplies(findReplies(c.getId(), allComments));
590+
ForumCommentVO vo = convertToCommentVO(c, authorMap);
591+
vo.setReplies(findReplies(c.getId(), allComments, authorMap));
538592
return vo;
539593
})
540594
.collect(Collectors.toList());

backend-spring/src/main/resources/application-dev.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ spring:
2020
jwt:
2121
secret: dev-secret-key-for-local-development-must-be-at-least-32-chars
2222
cookie:
23-
secure: false # Disable secure cookie for local development
23+
accessToken:
24+
secure: false # Disable secure flag for local development (HTTP)
25+
refreshToken:
26+
secure: false # Disable secure flag for local development (HTTP)
2427

2528
# MyBatis-Plus Development Settings
2629
mybatis-plus:

console/.eslintcache

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

console/src/api/forum.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import type {
77
ForumThread,
88
ForumTag,
99
} from "@/types/forum";
10-
import { fetchCurrentUserId } from "@/utils/auth";
1110

1211
export async function fetchForumPosts(): Promise<ForumPost[]> {
1312
return apiGet<ForumPost[]>("/forum/posts");
@@ -93,10 +92,7 @@ export async function deleteForumComment(commentId: string): Promise<void> {
9392
}
9493

9594
export async function recordForumView(postId: string) {
96-
const userId = fetchCurrentUserId();
97-
// Call the general view recording (with IP/cooldown logic)
98-
apiPost(`/views/forum/${postId}`, { userId }).catch(() => {});
99-
// Also call the forum-specific view recording to update stats JSON
95+
// Call the forum-specific view recording endpoint
10096
return apiPost(`/forum/posts/${postId}/view`, {});
10197
}
10298

console/src/components/bookmark/AddToBookmarkButton.vue

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
} from "@/api/bookmark";
1919
import type { BookmarkType } from "@/types/bookmark";
2020
import { toast } from "vue-sonner";
21-
import { isAuthenticated } from "@/utils/auth";
21+
import { useAuth } from "@/composables/useAuth";
2222
import { useI18n } from "vue-i18n";
2323
2424
const props = defineProps<{
@@ -34,11 +34,11 @@ const emit = defineEmits<{
3434
}>();
3535
const store = useBookmarkStore();
3636
const { t } = useI18n();
37+
const { isAuthenticated } = useAuth();
3738
3839
const itemFolders = ref<string[]>([]);
3940
const isLoading = ref(false);
4041
const isOpen = ref(false);
41-
const isAuthed = ref(false);
4242
4343
const isFavorited = computed(() => {
4444
const defaultId = store.defaultFolder?.id;
@@ -50,8 +50,7 @@ const isBookmarked = computed(() => itemFolders.value.length > 0);
5050
async function loadData() {
5151
if (!isOpen.value) return;
5252
53-
isAuthed.value = isAuthenticated();
54-
if (!isAuthed.value) {
53+
if (!isAuthenticated.value) {
5554
itemFolders.value = [];
5655
isLoading.value = false;
5756
return;
@@ -72,7 +71,7 @@ async function loadData() {
7271
}
7372
7473
async function toggleFolder(folderId: string) {
75-
if (!isAuthenticated()) {
74+
if (!isAuthenticated.value) {
7675
toast.error(t("bookmark.toasts.loginRequired"));
7776
return;
7877
}
@@ -138,7 +137,7 @@ watch(
138137
<Loader2 class="h-6 w-6 animate-spin text-primary/60" />
139138
</div>
140139
</template>
141-
<template v-else-if="!isAuthed">
140+
<template v-else-if="!isAuthenticated">
142141
<div class="px-4 py-6 text-center">
143142
<Bookmark class="h-8 w-8 text-muted-foreground/30 mx-auto mb-2" />
144143
<p

console/src/components/comments/comment-tree-builder.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,34 @@ const mapToComment = (
1919
const voteCounts = resolveVoteCounts(input.likes, input.dislikes);
2020
const userVote = resolveUserVote(input.userVote);
2121

22+
// Get username from backend response (authorUsername) or from author object
23+
const username = input.authorUsername || input.author?.username;
24+
const authorId = input.authorId || input.author?.id;
25+
const avatar = input.authorAvatar || input.author?.avatar;
26+
27+
// Validate required fields - these should always be present from backend
28+
if (!username) {
29+
console.error("Comment missing required username:", input);
30+
throw new Error(`Comment ${input.id} is missing required username field`);
31+
}
32+
if (!authorId) {
33+
console.error("Comment missing required authorId:", input);
34+
throw new Error(`Comment ${input.id} is missing required authorId field`);
35+
}
36+
2237
return {
2338
id: input.id,
24-
author: input.author.username,
25-
avatar: buildAvatar(input.author.username, input.author.avatar),
39+
author: username,
40+
avatar: buildAvatar(username, avatar),
2641
time: formatRelativeTime(input.createdAt),
2742
votes: voteCounts.likes - voteCounts.dislikes,
2843
likes: voteCounts.likes,
2944
dislikes: voteCounts.dislikes,
3045
userVote,
3146
content: input.body,
3247
isOp:
33-
!!options?.postAuthorUsername &&
34-
input.author.username === options.postAuthorUsername,
35-
isOwn:
36-
!!options?.currentUserId && input.author.id === options.currentUserId,
48+
!!options?.postAuthorUsername && username === options.postAuthorUsername,
49+
isOwn: !!options?.currentUserId && authorId === options.currentUserId,
3750
children: [],
3851
};
3952
};

0 commit comments

Comments
 (0)