Skip to content

Feat: Comments Plugin#92

Open
olliethedev wants to merge 62 commits intomainfrom
feat/comments-plugin
Open

Feat: Comments Plugin#92
olliethedev wants to merge 62 commits intomainfrom
feat/comments-plugin

Conversation

@olliethedev
Copy link
Collaborator

@olliethedev olliethedev commented Mar 14, 2026

Summary

Type of change

  • Bug fix
  • New plugin
  • Feature / enhancement to an existing plugin
  • Documentation
  • Chore / refactor / tooling

Checklist

  • pnpm build passes
  • pnpm typecheck passes
  • pnpm lint passes
  • Tests added or updated (unit and/or E2E)
  • Docs updated (docs/content/docs/) if consumer-facing types or behavior changed
  • All three example apps updated if a plugin was added or changed
  • New plugin: submission checklist in CONTRIBUTING.md completed

Screenshots

Screenshot 2026-03-14 at 4 07 33 PM

Note

Medium Risk
Introduces a new Comments plugin with new HTTP endpoints and example app wiring, which affects request handling and authorization hook configuration. CI/E2E changes are low risk but may increase runtime/flakiness due to new framework-matrix execution and added smoke coverage.

Overview
Adds a first-party Comments plugin with backend routes (create/edit/status/like/delete, counts, pagination) and client UI (embeddable CommentThread/CommentCount, moderation + “my comments” pages), exporting new entrypoints/CSS and bumping @btst/stack to 2.8.0.

Updates all three example apps to register the plugin on both client/server, import its CSS, and embed comment threads under blog posts and Kanban task details; also adds required PATCH handling to framework API catch-all routes.

Expands docs/README to include the new plugin and its integration points, and overhauls E2E to add smoke.comments coverage plus per-framework Playwright runs (via BTST_FRAMEWORK and a GitHub Actions matrix) with per-framework artifacts.

Written by Cursor Bugbot for commit 67483fb. This will update automatically on new commits. Configure here.

olliethedev and others added 30 commits March 12, 2026 16:09
…rePost hook, enhancing security and preventing client-side ID manipulation
…ormalizing parentId, ensuring accurate cache targeting
…rify access control for comment list and count requests
…ST response includes resolvedAuthorName and handling undefined values in getInitials function
…ies after page refresh, ensuring correct server-side handling of currentUserId
olliethedev and others added 18 commits March 13, 2026 20:13
…ort in smoke tests and enhance comment thread API queries
…r identification and pending comment visibility
…Id for e2e testing purposes across multiple examples
…ts with efficient pagination and sorting logic
…eceives complete data including resolved fields
…d, ensuring secure user identity resolution server-side
…eneration, improving code organization and reusability
…URL for demo purposes, enhancing security awareness in examples
…and improving navigation to enhance test reliability
… users and improving currentUserId handling for better user experience
@vercel
Copy link

vercel bot commented Mar 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
better-stack-docs Ready Ready Preview, Comment Mar 14, 2026 8:05pm

Request Review

// In production: replace with a real session/role check, e.g.:
// const session = await getSession(ctx.headers)
// if (!session?.user?.isAdmin) throw new Error("Admin access required")
console.log("onBeforeList: non-approved status filter — ensure admin check in production")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH: No-op hook bypasses built-in 403 guard.

This hook never throws, so the built-in protection for status=pending and status=spam queries is silently disabled. Without this hook, those requests would be automatically rejected with 403. A no-op hook is worse than no hook.

Either remove onBeforeList entirely, or add a real admin check:

onBeforeList: async (query, ctx) => {
    if (query.status && query.status !== 'approved') {
        const session = await getSession(ctx.headers)
        if (!session?.user?.isAdmin) throw new Error('Admin access required')
    }
},

// request URL (NOT secure — for testing only). In a real app, derive
// the user ID from the session cookie or JWT instead.
const url = new URL((ctx.request as Request).url)
return url.searchParams.get("currentUserId") ?? null
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH: Client-controlled user identity — any caller can impersonate any user.

url.searchParams.get('currentUserId') lets any unauthenticated HTTP client supply an arbitrary user ID and receive that user's pending comments and like state. This is labeled 'NOT secure' but is committed and will be copied. Replace with session-derived identity before this merges:

resolveCurrentUserId: async (ctx) => {
    const session = await getSession(ctx.headers)
    return session?.user?.id ?? null
},

},
onBeforeLike: async (commentId, authorId, ctx) => {
// In production: verify authorId matches the authenticated session
console.log("onBeforeLike: user", authorId, "toggling like on comment", commentId)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: authorId from the request body is never verified against the session.

The plugin docs warn that without this check any caller can toggle likes on behalf of arbitrary user IDs. This hook only logs — it never verifies the client-supplied authorId matches the authenticated session:

onBeforeLike: async (commentId, authorId, ctx) => {
    const session = await getSession(ctx.headers)
    if (!session?.user) throw new Error('Authentication required')
    if (session.user.id !== authorId) throw new Error('Forbidden')
},

},
onBeforeStatusChange: async (commentId, status, ctx) => {
// In production: verify the caller has admin/moderator role
console.log("onBeforeStatusChange: comment", commentId, "->", status)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: No-op hook — any caller can change comment status.

This hook never verifies the caller has moderator/admin privileges. onBeforeStatusChange absent → 403 by default. Hook provided but no-op → anyone can approve or spam any comment.

},
onBeforeDelete: async (commentId, ctx) => {
// In production: verify the caller has admin/moderator role
console.log("onBeforeDelete: comment", commentId)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: No-op hook — any caller can delete any comment.

Same pattern as onBeforeStatusChange: the hook is provided (disabling the built-in 403 guard) but never enforces any authorization check.

const { id } = ctx.params;
const context: CommentsApiContext = {
params: ctx.params,
body: ctx.body,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design observation: Unlike POST /comments (where authorId is resolved exclusively server-side via onBeforePost and is absent from the body schema), this endpoint accepts authorId from the client body and delegates verification to onBeforeLike. The examples show this delegation is easy to forget. Consider resolving the liker's identity via resolveCurrentUserId server-side — the same pattern used for POST — to eliminate the class of bugs where onBeforeLike forgets to cross-check the session.

…d React Router, enhancing testing capabilities across frameworks
);
} catch (error) {
throw error;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant try-catch blocks re-throw without transformation

Low Severity

Every endpoint handler in plugin.ts wraps its logic in try { … } catch (error) { throw error; }, which is a no-op — the error propagates identically without the try-catch. This pattern appears in all seven endpoint handlers (listComments, createComment, updateComment, getCommentCount, toggleLike, updateStatus, deleteComment), adding visual noise without providing any error transformation, logging, or recovery.

Additional Locations (2)
Fix in Cursor Fix in Web

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

There are 4 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

items: [...old.items, optimistic],
total: old.total + 1,
};
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimistic reply update hides existing approved replies

Medium Severity

When posting a reply to a parent whose replies section hasn't been expanded yet, the onMutate optimistic update in usePostComment finds no existing cache (old is undefined) and creates a single-item CommentListResult. After the mutation succeeds, handleReply calls setExpandedReplies to auto-expand, causing useComments in RepliesSection to enable. It finds the single-item cache, considers it fresh (within staleTime: 30_000), and skips refetching — so existing approved replies are hidden for up to 30 seconds.

Additional Locations (1)
Fix in Cursor Fix in Web

],
pageParams: [0],
};
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded limit in optimistic infinite fallback mismatches pageSize

Low Severity

When there's no existing infinite-query cache (!old), the optimistic update creates a fallback page with limit: 10 hardcoded. The actual pageSize used by useInfiniteComments (which could be 5, 100, or any configured value) is not propagated here. This causes getNextPageParam to compute the wrong nextOffset, potentially showing or hiding the "Load more" button incorrectly until the next real fetch.

Fix in Cursor Fix in Web

} else {
setResolvedUserId(rawCurrentUserId ?? undefined);
}
}, [rawCurrentUserId]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate currentUserId resolution logic across components

Low Severity

The currentUserId resolution logic (handling both static string and async function forms via useState + useEffect) is duplicated: inlined in resource-comments-page.tsx and extracted as useResolvedCurrentUserId in my-comments-page.internal.tsx. These are identical implementations that could share the extracted hook, reducing maintenance burden and risk of divergence.

Additional Locations (1)
Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Comments Plugin

1 participant