Skip to content

Removed member id/uuid from public comments API responses#28122

Open
9larsons wants to merge 1 commit into
mainfrom
comments-remove-member-uuid
Open

Removed member id/uuid from public comments API responses#28122
9larsons wants to merge 1 commit into
mainfrom
comments-remove-member-uuid

Conversation

@9larsons
Copy link
Copy Markdown
Contributor

Summary

  • The public comments endpoint exposed each comment author's id and uuid — stable identifiers that let any reader correlate every comment by a given member across a site.
  • The only consumer (apps/comments-ui) used them solely to check "is this comment mine?" for the edit/delete/expertise UI.
  • Replaced with a per-viewer is_author boolean computed server-side from the authenticated member session (same pattern as the existing liked field). The admin API response is unchanged.
  • Also dropped the data-member-uuid DOM attribute, which leaked the same identifier into the rendered page.

Test plan

  • pnpm --filter comments-ui test:unit passes
  • pnpm --filter comments-ui test:acceptance passes (mocked API now decorates responses with is_author)
  • cd ghost/core && pnpm test:single test/e2e-api/members-comments/comments.test.js passes against updated snapshots
  • Manual: log in as member, leave a comment, confirm edit/delete buttons appear; log in as a different member, confirm they do not

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 26, 2026

Review Change Stack

Walkthrough

This pull request refactors how the comments system determines whether a comment was authored by the current user. The change replaces UUID equality checks with an explicit is_author boolean flag. The Member type is updated to include the optional is_author field, frontend components are refactored to use this flag for authorship detection, and the memberUuid prop is removed from CommentLayout. The backend serialization layer excludes uuid from responses and computes is_author by comparing member identities. Test infrastructure is updated to populate the new field and adjust expected behavior accordingly.

Suggested reviewers

  • weylandswart

Possibly related PRs

  • TryGhost/Ghost#27962: Refactors CommentComponent/CommentLayout rendering props and overlaps with changes to CommentLayout interface and threading (memberUuid/data-member-uuid).
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the primary change: removing member id/uuid from the public comments API responses.
Description check ✅ Passed The PR description is directly related to the changeset, providing clear context about the privacy improvement and the technical approach taken.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch comments-remove-member-uuid

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.42.3)
ghost/core/test/e2e-api/members-comments/comments.test.js

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 26, 2026

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit 9b526d9

Command Status Duration Result
nx run ghost:test:ci:e2e ❌ Failed 24s View ↗
nx run ghost:test:ci:legacy ✅ Succeeded 3m 6s View ↗
nx build @tryghost/activitypub ✅ Succeeded 1s View ↗
nx build @tryghost/announcement-bar ✅ Succeeded <1s View ↗
nx build @tryghost/signup-form ✅ Succeeded <1s View ↗
nx build @tryghost/comments-ui ✅ Succeeded <1s View ↗
nx build @tryghost/portal ✅ Succeeded <1s View ↗
nx build @tryghost/sodo-search ✅ Succeeded <1s View ↗
Additional runs (7) ✅ Succeeded ... View ↗

☁️ Nx Cloud last updated this comment at 2026-05-26 00:29:06 UTC

- the public comments endpoint exposed every comment author's id and
  uuid, which is a stable identifier that lets any reader correlate all
  comments by a given member across a site
- the only consumer (comments-ui) used those identifiers solely to check
  "is this comment mine?" for the edit/delete/expertise UI
- replaced them with a per-viewer is_author boolean computed from the
  authenticated member session (same pattern as the existing liked
  field), so the client gets exactly what it needs and nothing more
- also dropped the data-member-uuid DOM attribute which leaked the same
  identifier into the rendered page
@9larsons 9larsons force-pushed the comments-remove-member-uuid branch from 9b526d9 to 0af4fcc Compare May 26, 2026 00:22
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/comments-ui/test/utils/mocked-api.ts`:
- Line 29: The constructor signature advertises a members?: any[] option but
never reads it; remove members from the constructor parameter type and the
destructured default list so callers can't pass a no-op option. Update the
constructor definition (the constructor({postId = 'ABC', comments = [], member =
undefined, settings = {}, labs = {}}: {postId?: string, comments?: any[],
member?: any, settings?: any, labs?: any}) declaration) by deleting the
members?: any[] entry from the type and ensuring the destructured params no
longer include members; if the class actually needs a members array instead,
alternatively accept members in the destructuring and assign it to the instance
(this.members) — pick one consistent approach and update tests/call sites
accordingly.
- Around line 238-245: `#withIsAuthor` currently derives is_author from the
instance-level this.member?.id which ignores request-scoped member UUIDs (e.g.,
admin impersonation), so update the API mock to accept a request-specific
memberId parameter and thread it through: change `#withIsAuthor` to signature like
`#withIsAuthor`(comment, memberId) and have browseComments and browseReplies pass
their request memberId (including impersonation paths) into the recursive calls
so each comment/reply computes is_author as comment.member?.id === memberId
rather than using this.member.

In `@ghost/core/test/e2e-api/members-comments/comments.test.js`:
- Around line 110-111: The test currently asserts on a removed field
(fetchedComment.member.id !== memberId); update the assertion to use the new
member contract by removing the id check and instead assert the PUT result via
fetchedComment.member.is_author (and/or other properties in commentMatcher).
Locate the assertion that references fetchedComment.member.id (near the
assertion around the PUT fetch) and replace it with an assertion that
fetchedComment.member.is_author matches the expected boolean after the PUT,
keeping the rest of the commentMatcher check intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 042d0730-0479-485e-9110-661344beb2a3

📥 Commits

Reviewing files that changed from the base of the PR and between 9b526d9 and 0af4fcc.

⛔ Files ignored due to path filters (1)
  • ghost/core/test/e2e-api/members-comments/__snapshots__/comments.test.js.snap is excluded by !**/*.snap
📒 Files selected for processing (8)
  • apps/comments-ui/src/app-context.ts
  • apps/comments-ui/src/components/content/comment.tsx
  • apps/comments-ui/src/components/content/context-menus/comment-context-menu.tsx
  • apps/comments-ui/test/unit/components/content/comment.test.jsx
  • apps/comments-ui/test/unit/components/content/context-menus/comment-context-menu.test.jsx
  • apps/comments-ui/test/utils/mocked-api.ts
  • ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js
  • ghost/core/test/e2e-api/members-comments/comments.test.js
💤 Files with no reviewable changes (1)
  • apps/comments-ui/test/unit/components/content/comment.test.jsx

}

constructor({postId = 'ABC', comments = [], member = undefined, settings = {}, members = [], labs = {}}: {postId?: string, comments?: any[], member?: any, settings?: any, members?: any[], labs?: any}) {
constructor({postId = 'ABC', comments = [], member = undefined, settings = {}, labs = {}}: {postId?: string, comments?: any[], member?: any, settings?: any, members?: any[], labs?: any}) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Constructor options still advertise members but silently ignore it.

members?: any[] remains in the constructor type, but the value is no longer read. This makes tests pass an option that has no effect and hides setup mistakes.

Suggested fix
-constructor({postId = 'ABC', comments = [], member = undefined, settings = {}, labs = {}}: {postId?: string, comments?: any[], member?: any, settings?: any, members?: any[], labs?: any}) {
+constructor({postId = 'ABC', comments = [], member = undefined, settings = {}, labs = {}}: {postId?: string, comments?: any[], member?: any, settings?: any, labs?: any}) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constructor({postId = 'ABC', comments = [], member = undefined, settings = {}, labs = {}}: {postId?: string, comments?: any[], member?: any, settings?: any, members?: any[], labs?: any}) {
constructor({postId = 'ABC', comments = [], member = undefined, settings = {}, labs = {}}: {postId?: string, comments?: any[], member?: any, settings?: any, labs?: any}) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/comments-ui/test/utils/mocked-api.ts` at line 29, The constructor
signature advertises a members?: any[] option but never reads it; remove members
from the constructor parameter type and the destructured default list so callers
can't pass a no-op option. Update the constructor definition (the
constructor({postId = 'ABC', comments = [], member = undefined, settings = {},
labs = {}}: {postId?: string, comments?: any[], member?: any, settings?: any,
labs?: any}) declaration) by deleting the members?: any[] entry from the type
and ensuring the destructured params no longer include members; if the class
actually needs a members array instead, alternatively accept members in the
destructuring and assign it to the instance (this.members) — pick one consistent
approach and update tests/call sites accordingly.

Comment on lines +238 to +245
#withIsAuthor(comment: any): any {
const memberId = this.member?.id;
return {
...comment,
member: comment.member ? {...comment.member, is_author: !!memberId && comment.member.id === memberId} : null,
replies: (comment.replies ?? []).map((reply: any) => this.#withIsAuthor(reply))
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

is_author derivation is global-member based, not request-scoped.

#withIsAuthor always compares against this.member?.id, so admin/impersonation paths that provide a request member UUID can be misrepresented in mocked responses. That can mask ownership-path regressions in tests.

Suggested direction
-#withIsAuthor(comment: any): any {
-    const memberId = this.member?.id;
+#withIsAuthor(comment: any, memberId?: string): any {
     return {
         ...comment,
         member: comment.member ? {...comment.member, is_author: !!memberId && comment.member.id === memberId} : null,
-        replies: (comment.replies ?? []).map((reply: any) => this.#withIsAuthor(reply))
+        replies: (comment.replies ?? []).map((reply: any) => this.#withIsAuthor(reply, memberId))
     };
 }

Then pass a request-specific memberId from browseComments/browseReplies call sites (including admin impersonation flows).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#withIsAuthor(comment: any): any {
const memberId = this.member?.id;
return {
...comment,
member: comment.member ? {...comment.member, is_author: !!memberId && comment.member.id === memberId} : null,
replies: (comment.replies ?? []).map((reply: any) => this.#withIsAuthor(reply))
};
}
`#withIsAuthor`(comment: any, memberId?: string): any {
return {
...comment,
member: comment.member ? {...comment.member, is_author: !!memberId && comment.member.id === memberId} : null,
replies: (comment.replies ?? []).map((reply: any) => this.#withIsAuthor(reply, memberId))
};
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/comments-ui/test/utils/mocked-api.ts` around lines 238 - 245,
`#withIsAuthor` currently derives is_author from the instance-level
this.member?.id which ignores request-scoped member UUIDs (e.g., admin
impersonation), so update the API mock to accept a request-specific memberId
parameter and thread it through: change `#withIsAuthor` to signature like
`#withIsAuthor`(comment, memberId) and have browseComments and browseReplies pass
their request memberId (including impersonation paths) into the recursive calls
so each comment/reply computes is_author as comment.member?.id === memberId
rather than using this.member.

Comment on lines 110 to +111
member: {
id: anyObjectId,
uuid: anyUuid
is_author: anyBoolean
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find test assertions or code that access member.id or member.uuid in comment responses

# Search for patterns like .member.id, .member.uuid, comment.member.id, etc.
rg -n --type=js -C3 '\b(comment|fetchedComment|body\.comments\[\d+\])\.member\.(id|uuid)\b' ghost/core/test/e2e-api/members-comments/

# Also check for destructuring patterns
rg -n --type=js -C3 'member:\s*\{\s*(id|uuid)' ghost/core/test/e2e-api/members-comments/

Repository: TryGhost/Ghost

Length of output: 710


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="ghost/core/test/e2e-api/members-comments/comments.test.js"

echo "== Matcher definition around lines 90-130 =="
nl -ba "$FILE" | sed -n '90,140p'

echo "== All occurrences of .member.id or .member.uuid =="
rg -n --type=js '\.member\.(id|uuid)\b' "$FILE"

echo "== Context around the line 1514 match =="
nl -ba "$FILE" | sed -n '1490,1535p'

echo "== Any usage of is_author in expectations =="
rg -n --type=js '\bis_author\b' "$FILE" | head -n 50

echo "== Any usage of memberId/memberUuid variables =="
rg -n --type=js '\bmember(Id|UUID|Uuid|UUID)\b' "$FILE"
rg -n --type=js '\bmemberId\b|\bmember_uuid\b|\bmember\.uuid\b' "$FILE"

Repository: TryGhost/Ghost

Length of output: 164


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="ghost/core/test/e2e-api/members-comments/comments.test.js"

echo "== Matcher definition around lines 90-140 =="
cat -n "$FILE" | sed -n '90,140p'

echo "== All occurrences of .member.id or .member.uuid =="
rg -n --type=js '\.member\.(id|uuid)\b' "$FILE"

echo "== Context around the line ~1514 match =="
cat -n "$FILE" | sed -n '1490,1535p'

echo "== Any usage of is_author in expectations =="
rg -n --type=js '\bis_author\b' "$FILE" | head -n 50

echo "== Any usage of memberId/memberUuid variables =="
rg -n --type=js '\bmember(Id|UUID|Uuid|UUID)\b' "$FILE" || true
rg -n --type=js '\bmemberId\b|\bmember_uuid\b|\bmember\.uuid\b' "$FILE" || true

Repository: TryGhost/Ghost

Length of output: 4447


Update the test assertion to avoid relying on removed member.id/member.uuid fields

commentMatcher now expects member: { is_author: anyBoolean } (no member.id/member.uuid). The only remaining direct dependency is assert(fetchedComment.member.id !== memberId); at ~line 1514; there are no .member.uuid usages in this test file. If the API no longer returns member.id, this check can silently become non-meaningful. Update the assertion to validate the PUT behavior using the new contract (e.g., fetchedComment.member.is_author) instead of fetchedComment.member.id.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ghost/core/test/e2e-api/members-comments/comments.test.js` around lines 110 -
111, The test currently asserts on a removed field (fetchedComment.member.id !==
memberId); update the assertion to use the new member contract by removing the
id check and instead assert the PUT result via fetchedComment.member.is_author
(and/or other properties in commentMatcher). Locate the assertion that
references fetchedComment.member.id (near the assertion around the PUT fetch)
and replace it with an assertion that fetchedComment.member.is_author matches
the expected boolean after the PUT, keeping the rest of the commentMatcher check
intact.

@github-actions
Copy link
Copy Markdown
Contributor

E2E Tests Failed

To view the Playwright test report locally, run:

REPORT_DIR=$(mktemp -d) && gh run download 26425306583 -n playwright-report -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR"

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.

1 participant