Skip to content

Commit 30e5595

Browse files
committed
✨ Added ability to ban members from commenting
ref https://linear.app/ghost/issue/FEA-487 Adds a `can_comment` boolean field to the Member model that allows admins to ban specific members from commenting. When set to false, the member cannot create new comments or replies.
1 parent 35adc0a commit 30e5595

File tree

8 files changed

+104
-4
lines changed

8 files changed

+104
-4
lines changed

ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ const memberFields = [
1919
'uuid',
2020
'name',
2121
'expertise',
22-
'avatar_image'
22+
'avatar_image',
23+
'can_comment'
2324
];
2425

2526
const memberFieldsAdmin = [
@@ -28,7 +29,8 @@ const memberFieldsAdmin = [
2829
'name',
2930
'email',
3031
'expertise',
31-
'avatar_image'
32+
'avatar_image',
33+
'can_comment'
3234
];
3335

3436
const postFields = [

ghost/core/core/server/api/endpoints/utils/serializers/output/members.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ function serializeMember(member, options) {
178178
email_recipients: json.email_recipients,
179179
status: json.status,
180180
last_seen_at: json.last_seen_at,
181+
can_comment: json.can_comment,
181182
attribution: serializeAttribution(json.attribution),
182183
unsubscribe_url: json.unsubscribe_url
183184
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const {createAddColumnMigration} = require('../../utils');
2+
3+
module.exports = createAddColumnMigration('members', 'can_comment', {
4+
type: 'boolean',
5+
nullable: false,
6+
defaultTo: true
7+
});

ghost/core/core/server/data/schema/schema.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@ module.exports = {
429429
note: {type: 'string', maxlength: 2000, nullable: true},
430430
geolocation: {type: 'string', maxlength: 2000, nullable: true},
431431
enable_comment_notifications: {type: 'boolean', nullable: false, defaultTo: true},
432+
can_comment: {type: 'boolean', nullable: false, defaultTo: true},
432433
email_count: {type: 'integer', unsigned: true, nullable: false, defaultTo: 0},
433434
email_opened_count: {type: 'integer', unsigned: true, nullable: false, defaultTo: 0},
434435
email_open_rate: {type: 'integer', unsigned: true, nullable: true, index: true},

ghost/core/core/server/models/member.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ const Member = ghostBookshelf.Model.extend({
1616
transient_id: crypto.randomUUID(),
1717
email_count: 0,
1818
email_opened_count: 0,
19-
enable_comment_notifications: true
19+
enable_comment_notifications: true,
20+
can_comment: true
2021
};
2122
},
2223

ghost/core/core/server/services/comments/CommentsService.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ const messages = {
1111
replyToReply: 'Can not reply to a reply',
1212
commentsNotEnabled: 'Comments are not enabled for this site.',
1313
cannotCommentOnPost: 'You do not have permission to comment on this post.',
14-
cannotEditComment: 'You do not have permission to edit comments'
14+
cannotEditComment: 'You do not have permission to edit comments',
15+
memberCannotComment: 'You do not have permission to comment.'
1516
};
1617

1718
class CommentsService {
@@ -61,6 +62,12 @@ class CommentsService {
6162

6263
/** @private */
6364
checkCommentAccess(memberModel) {
65+
if (memberModel.get('can_comment') === false) {
66+
throw new errors.NoPermissionError({
67+
message: tpl(messages.memberCannotComment)
68+
});
69+
}
70+
6471
if (this.enabled === 'paid' && memberModel.get('status') === 'free') {
6572
throw new errors.NoPermissionError({
6673
message: tpl(messages.cannotCommentOnPost)

ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,7 @@ module.exports = class MemberRepository {
505505
'products',
506506
'newsletters',
507507
'enable_comment_notifications',
508+
'can_comment',
508509
'last_seen_at',
509510
'last_commented_at',
510511
'expertise',

ghost/core/test/e2e-api/admin/members.test.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2397,6 +2397,86 @@ describe('Members API', function () {
23972397
});
23982398
});
23992399

2400+
describe('can_comment', function () {
2401+
let testMember;
2402+
2403+
beforeEach(async function () {
2404+
testMember = await createMember({
2405+
email: 'can-comment-test@test.com',
2406+
name: 'Test Member for can_comment'
2407+
});
2408+
});
2409+
2410+
afterEach(async function () {
2411+
await models.Member.destroy({id: testMember.id});
2412+
});
2413+
2414+
it('New members can comment by default', async function () {
2415+
should(testMember.get('can_comment')).be.true();
2416+
});
2417+
2418+
it('Can ban a member from commenting', async function () {
2419+
// Verify member can comment by default
2420+
should(testMember.get('can_comment')).be.true();
2421+
2422+
// Ban member from commenting
2423+
const {body} = await agent
2424+
.put(`/members/${testMember.id}/`)
2425+
.body({members: [{can_comment: false}]})
2426+
.expectStatus(200);
2427+
2428+
// Verify response includes can_comment: false
2429+
should(body.members[0].can_comment).be.false();
2430+
2431+
// Verify database was updated
2432+
await testMember.refresh();
2433+
should(testMember.get('can_comment')).be.false();
2434+
});
2435+
2436+
it('Can unban a member from commenting', async function () {
2437+
// First ban the member
2438+
await agent
2439+
.put(`/members/${testMember.id}/`)
2440+
.body({members: [{can_comment: false}]})
2441+
.expectStatus(200);
2442+
2443+
await testMember.refresh();
2444+
should(testMember.get('can_comment')).be.false();
2445+
2446+
// Now unban the member
2447+
const {body} = await agent
2448+
.put(`/members/${testMember.id}/`)
2449+
.body({members: [{can_comment: true}]})
2450+
.expectStatus(200);
2451+
2452+
// Verify response includes can_comment: true
2453+
should(body.members[0].can_comment).be.true();
2454+
2455+
// Verify database was updated
2456+
await testMember.refresh();
2457+
should(testMember.get('can_comment')).be.true();
2458+
});
2459+
2460+
it('Returns can_comment in member read response', async function () {
2461+
const {body} = await agent
2462+
.get(`/members/${testMember.id}/`)
2463+
.expectStatus(200);
2464+
2465+
should(body.members[0]).have.property('can_comment');
2466+
should(body.members[0].can_comment).be.true();
2467+
});
2468+
2469+
it('Returns can_comment in member browse response', async function () {
2470+
const {body} = await agent
2471+
.get(`/members/?filter=email:can-comment-test@test.com`)
2472+
.expectStatus(200);
2473+
2474+
should(body.members.length).be.greaterThan(0);
2475+
should(body.members[0]).have.property('can_comment');
2476+
should(body.members[0].can_comment).be.true();
2477+
});
2478+
});
2479+
24002480
// Log out
24012481
it('Can log out', async function () {
24022482
const member = await createMember({

0 commit comments

Comments
 (0)