diff --git a/public/language/en-GB/admin/manage/groups.json b/public/language/en-GB/admin/manage/groups.json index 814eebaa0f..c3d60d4eed 100644 --- a/public/language/en-GB/admin/manage/groups.json +++ b/public/language/en-GB/admin/manage/groups.json @@ -25,7 +25,8 @@ "edit.show-badge": "Show Badge", "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", - "edit.disable-requests": "Disable join requests", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", "edit.hidden": "Hidden", "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", "edit.add-user": "Add User to Group", diff --git a/public/language/en-GB/error.json b/public/language/en-GB/error.json index fb2951e188..9fed1f178f 100644 --- a/public/language/en-GB/error.json +++ b/public/language/en-GB/error.json @@ -117,6 +117,8 @@ "group-needs-owner": "This group requires at least one owner", "group-already-invited": "This user has already been invited", "group-already-requested": "Your membership request has already been submitted", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", "post-already-deleted": "This post has already been deleted", "post-already-restored": "This post has already been restored", diff --git a/public/language/en-GB/groups.json b/public/language/en-GB/groups.json index 9a37255a6b..abb34947cc 100644 --- a/public/language/en-GB/groups.json +++ b/public/language/en-GB/groups.json @@ -30,6 +30,7 @@ "details.latest_posts": "Latest Posts", "details.private": "Private", "details.disableJoinRequests": "Disable join requests", + "details.disableLeave": "Disallow users from leaving the group", "details.grant": "Grant/Rescind Ownership", "details.kick": "Kick", "details.kick_confirm": "Are you sure you want to remove this member from the group?", diff --git a/public/src/modules/helpers.js b/public/src/modules/helpers.js index 286327e10d..68584cc51a 100644 --- a/public/src/modules/helpers.js +++ b/public/src/modules/helpers.js @@ -160,7 +160,7 @@ // Groups helpers function membershipBtn(groupObj) { if (groupObj.isMember && groupObj.name !== 'administrators') { - return ''; + return ''; } if (groupObj.isPending && groupObj.name !== 'administrators') { diff --git a/src/groups/create.js b/src/groups/create.js index 8b8625cf9b..eb90b52ba2 100644 --- a/src/groups/create.js +++ b/src/groups/create.js @@ -13,6 +13,7 @@ module.exports = function (Groups) { if (data.name === 'administrators') { disableJoinRequests = 1; } + const disableLeave = parseInt(data.disableLeave, 10) === 1 ? 1 : 0; const isHidden = parseInt(data.hidden, 10) === 1; validateGroupName(data.name); @@ -36,6 +37,7 @@ module.exports = function (Groups) { system: isSystem ? 1 : 0, private: isPrivate, disableJoinRequests: disableJoinRequests, + disableLeave: disableLeave, }; plugins.fireHook('filter:group.create', { group: groupData, data: data }); diff --git a/src/groups/data.js b/src/groups/data.js index c50d3b263b..f00db8e04b 100644 --- a/src/groups/data.js +++ b/src/groups/data.js @@ -9,7 +9,7 @@ const utils = require('../utils'); const intFields = [ 'createtime', 'memberCount', 'hidden', 'system', 'private', - 'userTitleEnabled', 'disableJoinRequests', + 'userTitleEnabled', 'disableJoinRequests', 'disableLeave', ]; module.exports = function (Groups) { diff --git a/src/groups/update.js b/src/groups/update.js index 81c26c3171..2853ba705c 100644 --- a/src/groups/update.js +++ b/src/groups/update.js @@ -49,6 +49,10 @@ module.exports = function (Groups) { payload.disableJoinRequests = values.disableJoinRequests ? '1' : '0'; } + if (values.hasOwnProperty('disableLeave')) { + payload.disableLeave = values.disableLeave ? '1' : '0'; + } + await checkNameChange(groupName, values.name); if (values.hasOwnProperty('private')) { await updatePrivacy(groupName, values.private); diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index 71338f9117..b533f8ecba 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -43,7 +43,7 @@ SocketGroups.join = async (socket, data) => { }); if (results.groupData.private && results.groupData.disableJoinRequests) { - throw new Error('[[error:join-requests-disabled]]'); + throw new Error('[[error:group-join-disabled]]'); } if (!results.groupData.private || results.isAdmin) { @@ -68,6 +68,11 @@ SocketGroups.leave = async (socket, data) => { throw new Error('[[error:cant-remove-self-as-admin]]'); } + const groupData = await groups.getGroupData(data.groupName); + if (groupData.disableLeave) { + throw new Error('[[error:group-leave-disabled]]'); + } + await groups.leave(data.groupName, socket.uid); logGroupEvent(socket, 'group-leave', { groupName: data.groupName, diff --git a/src/views/admin/manage/group.tpl b/src/views/admin/manage/group.tpl index 273acde7d3..2e308358a3 100644 --- a/src/views/admin/manage/group.tpl +++ b/src/views/admin/manage/group.tpl @@ -69,7 +69,16 @@
+
+ + +
+
+
diff --git a/test/groups.js b/test/groups.js index 0b3dceba53..103a9cd3d7 100644 --- a/test/groups.js +++ b/test/groups.js @@ -38,6 +38,14 @@ describe('Groups', function () { disableJoinRequests: 0, }, next); }, + async () => { + await Groups.create({ + name: 'PrivateNoLeave', + description: 'Private group', + private: 1, + disableLeave: 1, + }); + }, function (next) { // Create a new user User.create({ @@ -62,8 +70,8 @@ describe('Groups', function () { }, ], function (err, results) { assert.ifError(err); - testUid = results[3]; - adminUid = results[4]; + testUid = results[4]; + adminUid = results[5]; Groups.join('administrators', adminUid, done); }); }); @@ -72,7 +80,7 @@ describe('Groups', function () { it('should list the groups present', function (done) { Groups.getGroupsFromSet('groups:visible:createtime', 0, -1, function (err, groups) { assert.ifError(err); - assert.equal(groups.length, 4); + assert.equal(groups.length, 5); done(); }); }); @@ -120,7 +128,7 @@ describe('Groups', function () { it('should return the groups when search query is empty', function (done) { socketGroups.search({ uid: adminUid }, { query: '' }, function (err, groups) { assert.ifError(err); - assert.equal(4, groups.length); + assert.equal(5, groups.length); done(); }); }); @@ -730,11 +738,21 @@ describe('Groups', function () { it('should fail to join if group is private and join requests are disabled', function (done) { meta.config.allowPrivateGroups = 1; socketGroups.join({ uid: testUid }, { groupName: 'PrivateNoJoin' }, function (err) { - assert.equal(err.message, '[[error:join-requests-disabled]]'); + assert.equal(err.message, '[[error:group-join-disabled]]'); done(); }); }); + it('should fail to leave if group is private and leave is disabled', async () => { + await socketGroups.join({ uid: testUid }, { groupName: 'PrivateNoLeave' }); + + try { + await socketGroups.leave({ uid: testUid }, { groupName: 'PrivateNoLeave' }); + } catch (err) { + assert.equal(err.message, '[[error:group-leave-disabled]]'); + } + }); + it('should join if user is admin', function (done) { socketGroups.join({ uid: adminUid }, { groupName: 'PrivateCanJoin' }, function (err) { assert.ifError(err);