Skip to content

Added Admin API endpoint for creating comments with member impersonation #23894

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

55sketch
Copy link
Contributor

ref CON-301

This change allows comment creation via Admin API, opening up the possibility to import comments from other platforms by enabling external tools to create comments on behalf of existing members.

ref CON-301
This change allows comment creation via Admin API, opening up the possibility to import comments from other platforms by enabling external tools to create comments on behalf of existing members.
Copy link
Contributor

coderabbitai bot commented Jun 17, 2025

Walkthrough

A new "add" endpoint was introduced to the comments API, enabling creation of comments with required fields for member ID and HTML content, and requiring either a post ID (for top-level comments) or a parent ID (for replies). This endpoint supports an optional created_at timestamp, validated to ensure it is a past date. The integration token allowlist was updated to permit GET, POST, and PUT methods for the "comments" endpoint. A corresponding POST route was registered in the admin API routes. The CommentsController and CommentsService were updated to accept and handle the optional custom creation timestamp when creating comments or replies. End-to-end tests were added to verify successful comment and reply creation, including with custom timestamps, validation error handling for missing fields, invalid timestamps, and ensuring no email notifications are sent on comment creation via the Admin API. No changes were made to existing exported or public entity signatures.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 03fdee8 and 0dd9731.

📒 Files selected for processing (1)
  • ghost/core/core/server/web/api/endpoints/admin/routes.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • ghost/core/core/server/web/api/endpoints/admin/routes.js
✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
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: 0

🧹 Nitpick comments (4)
ghost/core/core/server/web/api/endpoints/admin/middleware.js (1)

69-70: Allow-listing looks correct; consider documenting the new surface area

comments: ['GET', 'POST', 'PUT'] aligns with the new routes and deliberately omits DELETE. From a security/ops standpoint it would be helpful to add a short comment or update the admin-API docs so that integrators are aware that comment deletion is not allowed through integration tokens.

Please confirm whether docs/generated OpenAPI spec also lists the new permissions; if not, they should be regenerated.

ghost/core/test/e2e-api/admin/comments-add.test.js (2)

13-44: Happy-path test is solid; add an assertion for Location header

Besides validating the body, asserting res.headers.location (or lack thereof) would fully exercise the 201 semantics and catch regressions if the controller later decides to send a canonical URL.

-const response = await agent
-    .post('comments/')
-    .body({comments: [commentData]})
-    .expectStatus(201);
+const response = await agent
+    .post('comments/')
+    .body({comments: [commentData]})
+    .expectStatus(201);
+
+// Optional: verify Location header if the API ever sets it
+should.exist(response.headers);
+// should.exist(response.headers.location);

46-61: Fixtures not initialised for unauthenticated agent – harmless but noisy

The 401 check doesn’t need fixtures, yet the post/member IDs supplied are obviously invalid. To avoid future confusion, consider trimming the payload to {} or adding a comment that IDs are irrelevant because authentication fails first.

ghost/core/core/server/api/endpoints/comments.js (1)

112-119: Cache-header toggle is contradictory

headers.cacheInvalidate is set to false, yet custom invalidation headers are still emitted via handleCacheHeaders. That works, but it’s easy for a future maintainer to misinterpret the intent.

Add a short code comment explaining why manual invalidation is desirable here while the automatic mechanism is disabled.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dc0816a and e0f4ba9.

📒 Files selected for processing (4)
  • ghost/core/core/server/api/endpoints/comments.js (1 hunks)
  • ghost/core/core/server/web/api/endpoints/admin/middleware.js (1 hunks)
  • ghost/core/core/server/web/api/endpoints/admin/routes.js (1 hunks)
  • ghost/core/test/e2e-api/admin/comments-add.test.js (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
ghost/core/core/server/web/api/endpoints/admin/routes.js (1)
ghost/core/core/server/web/api/endpoints/content/routes.js (3)
  • router (12-12)
  • mw (5-5)
  • api (3-3)
ghost/core/core/server/api/endpoints/comments.js (2)
ghost/core/core/server/services/comments/CommentsController.js (1)
  • data (161-161)
ghost/core/core/server/api/endpoints/comment-replies.js (1)
  • commentsService (3-3)
⏰ Context from checks skipped due to timeout of 90000ms (8)
  • GitHub Check: Acceptance tests (Node 22.13.1, mysql8)
  • GitHub Check: Acceptance tests (Node 20.11.1, mysql8)
  • GitHub Check: Unit tests (Node 22.13.1)
  • GitHub Check: Legacy tests (Node 20.11.1, sqlite3)
  • GitHub Check: Unit tests (Node 20.11.1)
  • GitHub Check: Legacy tests (Node 20.11.1, mysql8)
  • GitHub Check: Ghost-CLI tests
  • GitHub Check: Lint
🔇 Additional comments (3)
ghost/core/core/server/web/api/endpoints/admin/routes.js (1)

46-51: Route addition LGTM

The POST route is placed next to the other comment endpoints and follows existing conventions (mw.authAdminApi, http(api.*) wrapper). Express’ default non-strict routing will match both /comments and /comments/, so test paths with a trailing slash will pass.

No further issues spotted.

ghost/core/core/server/api/endpoints/comments.js (2)

69-72: Good catch returning the browse result

Returning result prevents an implicit undefined from leaking to the transport layer.


73-122: ```shell
#!/bin/bash
set -e
echo "Searching for frame.options.context usage across the repository"
rg --color=never "frame\.options\.context" -n .


</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Copy link
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: 1

🧹 Nitpick comments (2)
ghost/core/test/e2e-api/admin/comments.test.js (2)

695-704: Keep header-snapshot parity with the rest of the suite

Almost every request assertion above records the etag header to guard against accidental cache regressions.
For consistency add the same check here:

-const response = await adminApi
-    .post('comments/')
-    .body({comments: [commentData]})
-    .expectStatus(201);
+const response = await adminApi
+    .post('comments/')
+    .body({comments: [commentData]})
+    .expectStatus(201)
+    .matchHeaderSnapshot({etag: anyEtag});

712-721: Strengthen the negative-path test – assert the error payload

The test only checks for a 400 status. Verifying the error structure (type, message, property names) protects against silent contract breaks.

 await adminApi
     .post('comments/')
     .body({comments: [commentData]})
-    .expectStatus(400);
+    .expectStatus(400)
+    .matchBodySnapshot({
+        errors: [{
+            type: 'ValidationError'
+        }]
+    });
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8d8aa23 and fd7be20.

📒 Files selected for processing (1)
  • ghost/core/test/e2e-api/admin/comments.test.js (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: Ghost-CLI tests
  • GitHub Check: Legacy tests (Node 20.11.1, sqlite3)
  • GitHub Check: Acceptance tests (Node 20.11.1, sqlite3)
  • GitHub Check: Unit tests (Node 22.13.1)
  • GitHub Check: Unit tests (Node 20.11.1)
  • GitHub Check: Legacy tests (Node 20.11.1, mysql8)
  • GitHub Check: Acceptance tests (Node 22.13.1, mysql8)
  • GitHub Check: Acceptance tests (Node 20.11.1, mysql8)
  • GitHub Check: Lint

Copy link
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: 1

🔭 Outside diff range comments (2)
ghost/core/core/server/services/comments/CommentsController.js (1)

159-172: Missing trust-boundary check between member_id in payload and impersonated member
The Admin endpoint relies on the caller to set frame.options.context.member.id (via the API layer) but the body also contains an explicit member_id. A malicious client could supply mismatching values and create comments on behalf of a different member.
Add a guard before calling the service layer:

+        // Ensure the request body cannot impersonate an unintended member
+        if (data.member_id && data.member_id !== frame.options.context.member.id) {
+            throw new errors.BadRequestError({
+                message: 'member_id does not match impersonated member'
+            });
+        }
ghost/core/core/server/services/comments/CommentsService.js (1)

219-256: Validate and normalise createdAt before persisting
createdAt can be any value – including invalid strings or numbers – and is written straight to the database. Perform a sanity check and convert to a Date object:

-        if (createdAt) {
-            commentData.created_at = createdAt;
+        if (createdAt) {
+            const ts = new Date(createdAt);
+            if (Number.isNaN(ts.getTime())) {
+                throw new errors.ValidationError({
+                    message: 'Invalid created_at timestamp'
+                });
+            }
+            commentData.created_at = ts;
         }

Apply the same validation in replyToComment.

🧹 Nitpick comments (3)
ghost/core/core/server/services/comments/CommentsService.js (1)

330-344: Duplicate logic – extract helper to build comment payload
Both commentOnPost and replyToComment build almost identical commentData objects. Consider extracting this into a small private method to keep the service DRY and easier to change in one place.

ghost/core/test/e2e-api/admin/comments.test.js (2)

731-735: Fragile equality assertion on created_at
The API may return the timestamp with millisecond precision or a different UTC offset, causing this strict equality check to break. Prefer the existing anyISODateTime matcher or compare with new Date(reply.created_at).getTime() === new Date(customTimestamp).getTime().


807-828: Missing body validation assertions
The negative-path tests only assert on status codes. Also assert the error type/message so that un-related validation failures don’t pass silently.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9bd0cf0 and 93d344f.

📒 Files selected for processing (4)
  • ghost/core/core/server/api/endpoints/comments.js (3 hunks)
  • ghost/core/core/server/services/comments/CommentsController.js (1 hunks)
  • ghost/core/core/server/services/comments/CommentsService.js (5 hunks)
  • ghost/core/test/e2e-api/admin/comments.test.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • ghost/core/core/server/api/endpoints/comments.js
🧰 Additional context used
🧬 Code Graph Analysis (1)
ghost/core/test/e2e-api/admin/comments.test.js (2)
ghost/core/test/e2e-api/members-comments/comments.test.js (30)
  • commentData (60-60)
  • comment (525-527)
  • comment (532-534)
  • comment (550-552)
  • comment (558-560)
  • comment (949-951)
  • comment (971-973)
  • comment (984-986)
  • comment (1054-1056)
  • comment (1074-1076)
  • comment (1082-1085)
  • comment (1105-1107)
  • comment (1126-1128)
  • comment (1150-1152)
  • comment (1169-1171)
  • dbFns (18-102)
  • dbFns (886-891)
  • dbFns (901-907)
  • dbFns (915-921)
  • dbFns (1013-1018)
  • dbFns (1038-1043)
  • dbFns (1217-1224)
  • reply (987-990)
  • reply (1322-1328)
  • reply (1341-1346)
  • reply (1379-1385)
  • reply (1404-1409)
  • reply (1428-1433)
  • reply (1456-1462)
  • reply (1483-1490)
ghost/core/test/unit/server/data/schema/fixtures/fixture-manager.test.js (1)
  • fixtureManager (9-9)
🪛 Biome (1.9.4)
ghost/core/core/server/services/comments/CommentsService.js

[error] 334-334: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: Acceptance tests (Node 22.13.1, mysql8)
  • GitHub Check: Unit tests (Node 20.11.1)
  • GitHub Check: Unit tests (Node 22.13.1)
  • GitHub Check: Legacy tests (Node 20.11.1, sqlite3)
  • GitHub Check: Acceptance tests (Node 20.11.1, sqlite3)
  • GitHub Check: Legacy tests (Node 20.11.1, mysql8)
  • GitHub Check: Ghost-CLI tests
  • GitHub Check: Acceptance tests (Node 20.11.1, mysql8)
  • GitHub Check: Lint

validateCommentData(data);
setupMemberContext(frame, data.member_id);

const result = await commentsService.controller.add(frame);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not necessarily a blocker, but I think this would be simpler to just call the commentsService functions directly, rather than going through the controller — i.e. instead of commentsService.controller.add(), just call commentsService.commentOnPost().

This would remove the need for the setupMemberContext function

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will look at this 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This change has been made in the latest version 👍

@cmraible
Copy link
Collaborator

Something else worth confirming: does this send any webhooks or email notifications for comments when it's used by an integration? I'd assume in most cases we wouldn't want that, but that behavior might be automatic, in which case we'd need to carve out some kind of exception.

Copy link
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: 1

🧹 Nitpick comments (4)
ghost/core/core/server/api/endpoints/comments.js (1)

24-41: Date-validation helper is duplicated & slightly leaky

  1. The same logic lives in CommentsController.#validateCreatedAt.
  2. The helper silently swallows invalid/future dates by returning undefined; callers have to know this convention. Exposing an explicit error (or at least a comment) would be safer.
  3. Converting a Date instance with new Date(createdAt) allocates unnecessarily.

Consider exporting a single helper from the service layer:

- const date = new Date(createdAt);
+ const date = createdAt instanceof Date ? createdAt : new Date(createdAt);

…and re-using it from both places.

ghost/core/test/e2e-api/admin/comments.test.js (3)

730-738: Fragile created_at equality assertion

Relying on an exact string match for created_at is brittle—DB precision or timezone normalisation can cause false negatives.

- comment.should.have.property('created_at', customTimestamp);
+ new Date(comment.created_at).toISOString().should.eql(customTimestamp);

830-892: Time-window assertion can flake on slow CI

timeDiff.should.be.lessThan(10000); gives the test 10 s headroom, yet a busy CI runner or debugging run can exceed that and produce intermittent failures.

Consider asserting ordering instead of an absolute delta:

new Date(comment.created_at).should.be.belowOrEqual(new Date());

678-1075: Heavy duplication – factor helper to DRY the test setup

The new suite repeats the same three-step pattern (build payload → POST → common assertions) ~10 times. Extracting a small helper (e.g. createCommentViaAdmin(payload)) would shrink the suite and improve readability/maintenance.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 93d344f and abeb5bf.

📒 Files selected for processing (3)
  • ghost/core/core/server/api/endpoints/comments.js (3 hunks)
  • ghost/core/core/server/services/comments/CommentsController.js (1 hunks)
  • ghost/core/test/e2e-api/admin/comments.test.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • ghost/core/core/server/services/comments/CommentsController.js
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Setup
🔇 Additional comments (2)
ghost/core/core/server/api/endpoints/comments.js (1)

132-136: Overwrites the existing request context

frame.options.context may already contain api_key, user, etc.
Replacing it wholesale risks losing that metadata for downstream hooks/middleware.

- if (!frame.options.context) {
-     frame.options.context = {};
- }
- frame.options.context.internal = true;
+ frame.options.context = {...frame.options.context, internal: true};

Likely an incorrect or invalid review comment.

ghost/core/test/e2e-api/admin/comments.test.js (1)

806-815: Expected status code may be wrong

Validation failures in Ghost usually surface as 422 Unprocessable Entity (errors.ValidationError).
If the framework keeps that convention, this test will fail.

- .expectStatus(400);
+ .expectStatus(422);

Please verify the actual response code.

@55sketch
Copy link
Contributor Author

Something else worth confirming: does this send any webhooks or email notifications for comments when it's used by an integration? I'd assume in most cases we wouldn't want that, but that behavior might be automatic, in which case we'd need to carve out some kind of exception.

@cmraible not sure why I couldn't reply to this directly ¯_(ツ)_/¯

We set the context to 'internal' so it doesn't kick off emails. I've also added a test to ensure this

Copy link
Collaborator

Choose a reason for hiding this comment

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

Looking good! I think all the changes in this file are not needed now, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How come? Would we not still want to validate the created_at value? We don't validate inside commentOnPost or replyToComment as far as I can tell.

(FWIW this change was added in response to a coderabbit review comment, so I'm not married to it - that being said it seems to make sense to have it)

55sketch and others added 2 commits June 19, 2025 09:36
Co-authored-by: Chris Raible <chris@ghost.org>
Co-authored-by: Chris Raible <chris@ghost.org>
Copy link
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: 0

🧹 Nitpick comments (1)
ghost/core/core/server/services/comments/CommentsService.js (1)

334-334: Consider using optional chaining for safer property access.

The static analysis tool suggests using optional chaining here for safer property access.

Apply this diff to use optional chaining:

-            in_reply_to_id: inReplyToComment && inReplyToComment.get('id'),
+            in_reply_to_id: inReplyToComment?.get('id'),
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75bb3de and 03fdee8.

📒 Files selected for processing (1)
  • ghost/core/core/server/services/comments/CommentsService.js (5 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
ghost/core/core/server/services/comments/CommentsService.js

[error] 334-334: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Setup
🔇 Additional comments (5)
ghost/core/core/server/services/comments/CommentsService.js (5)

207-207: LGTM: Bug fix for error property name.

Good catch fixing the typo from messages to message in the error object property.


219-221: LGTM: Parameter addition follows JSDoc best practices.

The optional parameter syntax and function signature update are correctly implemented.


243-255: LGTM: Clean implementation of conditional timestamp assignment.

The approach of building the comment data object first and then conditionally adding the created_at field is clean and readable. This avoids object spread complications and makes the conditional logic clear.


277-279: LGTM: Consistent parameter addition across methods.

The JSDoc and function signature updates for replyToComment are consistent with the commentOnPost method changes.


330-343: LGTM: Consistent implementation pattern for replies.

The implementation follows the same clean pattern as commentOnPost, maintaining consistency across the codebase.

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.

2 participants