diff --git a/public/.eslintrc b/public/.eslintrc index d328902f77da..2808ade27d3b 100644 --- a/public/.eslintrc +++ b/public/.eslintrc @@ -17,7 +17,7 @@ "jquery": true, "amd": true, "browser": true, - "es6": false + "es6": true }, "rules": { "block-scoped-var": "off", @@ -31,9 +31,9 @@ "prefer-template": "off" }, "parserOptions": { - "ecmaVersion": 5, + "ecmaVersion": 6, "ecmaFeatures": { - "arrowFunctions": false, + "arrowFunctions": true, "classes": false, "defaultParams": false, "destructuring": false, diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 709bfecdef99..ccb94eccfca6 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -419,6 +419,43 @@ paths: $ref: '#/components/schemas/Status' response: $ref: components/schemas/GroupObject.yaml#/GroupDataObject + delete: + tags: + - groups + summary: Delete an existing group + description: This operation deletes an existing group, all members within this group will cease to be members after the group is deleted. + responses: + '200': + description: group successfully deleted + content: + application/json: + schema: + type: object + properties: + status: + $ref: '#/components/schemas/Status' + response: + type: object + properties: {} + /groups/{slug}/membership/{uid}: + put: + tags: + - groups + summary: Join a group + description: This operation joins an existing group, or causes another user to join a group. If the group is private and you are not an administrator, this method will cause that user to request membership, instead. For user _invitations_, you'll want to call `PUT /groups/{slug}/invites/{uid}`. + responses: + '200': + description: group successfully joined, or membership requested + content: + application/json: + schema: + type: object + properties: + status: + $ref: '#/components/schemas/Status' + response: + type: object + properties: {} components: schemas: Status: diff --git a/public/src/admin/manage/admins-mods.js b/public/src/admin/manage/admins-mods.js index c2522aeea355..4aa3bfceee8d 100644 --- a/public/src/admin/manage/admins-mods.js +++ b/public/src/admin/manage/admins-mods.js @@ -1,6 +1,6 @@ 'use strict'; -define('admin/manage/admins-mods', ['translator', 'benchpress', 'autocomplete'], function (translator, Benchpress, autocomplete) { +define('admin/manage/admins-mods', ['translator', 'benchpress', 'autocomplete', 'api'], function (translator, Benchpress, autocomplete, api) { var AdminsMods = {}; AdminsMods.init = function () { @@ -42,13 +42,7 @@ define('admin/manage/admins-mods', ['translator', 'benchpress', 'autocomplete'], }); autocomplete.user($('#global-mod-search'), function (ev, ui) { - socket.emit('admin.groups.join', { - groupName: 'Global Moderators', - uid: ui.item.user.uid, - }, function (err) { - if (err) { - return app.alertError(err.message); - } + api.put('/groups/global-moderators/membership/' + ui.item.user.uid, undefined, () => { app.alertSuccess('[[admin/manage/users:alerts.make-global-mod-success]]'); $('#global-mod-search').val(''); @@ -60,7 +54,7 @@ define('admin/manage/admins-mods', ['translator', 'benchpress', 'autocomplete'], $('.global-moderator-area').prepend(html); $('#no-global-mods-warning').addClass('hidden'); }); - }); + }, err => app.alertError(err.status.message)); }); $('.global-moderator-area').on('click', '.remove-user-icon', function () { diff --git a/public/src/admin/manage/users.js b/public/src/admin/manage/users.js index b7cad0830811..64a1ee8c96e6 100644 --- a/public/src/admin/manage/users.js +++ b/public/src/admin/manage/users.js @@ -93,15 +93,12 @@ define('admin/manage/users', ['translator', 'benchpress', 'autocomplete', 'api'] modal.on('shown.bs.modal', function () { autocomplete.group(modal.find('.group-search'), function (ev, ui) { var uid = $(ev.target).attr('data-uid'); - socket.emit('admin.groups.join', { uid: uid, groupName: ui.item.value }, function (err) { - if (err) { - return app.alertError(err); - } + api.put('/groups/' + ui.item.group.slug + '/membership/' + uid, undefined, () => { ui.item.group.nameEscaped = translator.escape(ui.item.group.displayName); app.parseAndTranslate('admin/partials/manage_user_groups', { users: [{ groups: [ui.item.group] }] }, function (html) { $('[data-uid=' + uid + '] .group-area').append(html.find('.group-area').html()); }); - }); + }, err => app.alertError(err.status.message)); }); }); modal.on('click', '.group-area a', function () { diff --git a/src/controllers/write/groups.js b/src/controllers/write/groups.js index f253da51b37b..f7a317507ebe 100644 --- a/src/controllers/write/groups.js +++ b/src/controllers/write/groups.js @@ -1,7 +1,9 @@ 'use strict'; +const user = require('../../user'); const groups = require('../../groups'); const events = require('../../events'); +const meta = require('../../meta'); const helpers = require('../helpers'); @@ -26,6 +28,66 @@ Groups.create = async (req, res) => { }); }; +Groups.delete = async (req, res) => { + const group = await groups.getByGroupslug(req.params.slug, { + uid: req.user.uid, + }); + + if (groups.ephemeralGroups.includes(group.slug)) { + throw new Error('[[error:not-allowed]]'); + } + + if (group.system || (!group.isOwner && !res.locals.privileges.isAdmin && !res.locals.privileges.isGmod)) { + throw new Error('[[error:no-privileges]]'); + } + + await groups.destroy(group.name); + helpers.formatApiResponse(200, res); + logGroupEvent(req, 'group-delete', { + groupName: group.name, + }); +}; + +Groups.join = async (req, res) => { + const group = await groups.getByGroupslug(req.params.slug, { + uid: req.params.uid, + }); + const [isCallerOwner, userExists] = await Promise.all([ + groups.ownership.isOwner(req.user.uid, group.name), + user.exists(req.user.uid), + ]); + + if (group.isMember) { + // No change + return helpers.formatApiResponse(200, res); + } else if (!userExists) { + throw new Error('[[error:invalid-uid]]'); + } + + // console.log(res.locals.privileges); + // return res.sendStatus(200); + + if (!res.locals.privileges.isAdmin) { + // Admin and privilege groups unjoinable client-side + if (group.name === 'administrators' || groups.isPrivilegeGroup(group.name)) { + throw new Error('[[error:not-allowed]]'); + } + + if (!isCallerOwner && parseInt(meta.config.allowPrivateGroups, 10) !== 0 && group.private) { + await groups.requestMembership(group.name, req.params.uid); + } else { + await groups.join(group.name, req.params.uid); + } + } else { + await groups.join(group.name, req.params.uid); + } + + helpers.formatApiResponse(200, res); + logGroupEvent(req, 'group-join', { + groupName: group.name, + }); +}; + function logGroupEvent(req, event, additional) { events.log({ type: event, diff --git a/src/groups/index.js b/src/groups/index.js index a6db512ef6b2..d2b2450ccdaa 100644 --- a/src/groups/index.js +++ b/src/groups/index.js @@ -190,6 +190,7 @@ Groups.getOwnersAndMembers = async function (groupName, uid, start, stop) { }; Groups.getByGroupslug = async function (slug, options) { + options = options || {}; const groupName = await db.getObjectField('groupslug:groupname', slug); if (!groupName) { throw new Error('[[error:no-group]]'); diff --git a/src/middleware/assert.js b/src/middleware/assert.js new file mode 100644 index 000000000000..3b80c166a940 --- /dev/null +++ b/src/middleware/assert.js @@ -0,0 +1,20 @@ +'use strict'; + +/** + * The middlewares here strictly act to "assert" validity of the incoming + * payload and throw an error otherwise. + */ + +const groups = require('../groups'); + +module.exports = function (middleware) { + middleware.assertGroup = async (req, res, next) => { + const name = await groups.getGroupNameByGroupSlug(req.params.slug); + const exists = await groups.exists(name); + if (!exists) { + throw new Error('[[error:no-group]]'); + } + + next(); + }; +}; diff --git a/src/middleware/index.js b/src/middleware/index.js index b3d3ea4915f0..25eddd0fc5ea 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -60,6 +60,7 @@ require('./maintenance')(middleware); require('./user')(middleware); require('./headers')(middleware); require('./expose')(middleware); +require('./assert')(middleware); middleware.stripLeadingSlashes = function stripLeadingSlashes(req, res, next) { var target = req.originalUrl.replace(nconf.get('relative_path'), ''); diff --git a/src/routes/write/groups.js b/src/routes/write/groups.js index 1356299a7ced..474bc60f0952 100644 --- a/src/routes/write/groups.js +++ b/src/routes/write/groups.js @@ -11,33 +11,8 @@ module.exports = function () { const middlewares = [middleware.authenticate]; setupApiRoute(router, '/', middleware, [...middlewares, middleware.checkRequired.bind(null, ['name']), middleware.exposePrivilegeSet], 'post', controllers.write.groups.create); - // setupApiRoute(router, '/:slug', middleware, [...middlewares, middleware.exposePrivilegeSet], 'delete', controllers.write.groups.delete); - - // app.delete('/:slug', apiMiddleware.requireUser, middleware.exposeGroupName, apiMiddleware.validateGroup, apiMiddleware.requireGroupOwner, function(req, res) { - // Groups.destroy(res.locals.groupName, function(err) { - // errorHandler.handle(err, res); - // }); - // }); - - // app.put('/:slug/membership', apiMiddleware.requireUser, middleware.exposeGroupName, apiMiddleware.validateGroup, function(req, res) { - // if (Meta.config.allowPrivateGroups !== '0') { - // Groups.isPrivate(res.locals.groupName, function(err, isPrivate) { - // if (isPrivate) { - // Groups.requestMembership(res.locals.groupName, req.user.uid, function(err) { - // errorHandler.handle(err, res); - // }); - // } else { - // Groups.join(res.locals.groupName, req.user.uid, function(err) { - // errorHandler.handle(err, res); - // }); - // } - // }); - // } else { - // Groups.join(res.locals.groupName, req.user.uid, function(err) { - // errorHandler.handle(err, res); - // }); - // } - // }); + setupApiRoute(router, '/:slug', middleware, [...middlewares, middleware.assertGroup, middleware.exposePrivileges], 'delete', controllers.write.groups.delete); + setupApiRoute(router, '/:slug/membership/:uid', middleware, [...middlewares, middleware.assertGroup, middleware.exposePrivileges], 'put', controllers.write.groups.join); // app.put('/:slug/membership/:uid', middleware.exposeGroupName, apiMiddleware.validateGroup, apiMiddleware.requireUser, apiMiddleware.requireAdmin, function(req, res) { // Groups.join(res.locals.groupName, req.params.uid, function(err) { diff --git a/src/socket.io/admin/groups.js b/src/socket.io/admin/groups.js index 5cb14d9d5013..e98bdf66a1ae 100644 --- a/src/socket.io/admin/groups.js +++ b/src/socket.io/admin/groups.js @@ -24,6 +24,8 @@ Groups.create = async function (socket, data) { }; Groups.join = async (socket, data) => { + sockets.warnDeprecated(socket, 'PUT /api/v1/groups/:slug/membership/:uid'); + if (!data) { throw new Error('[[error:invalid-data]]'); } diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index e1b7dcb4a8af..850e18435123 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -19,6 +19,8 @@ SocketGroups.before = async (socket, method, data) => { }; SocketGroups.join = async (socket, data) => { + sockets.warnDeprecated(socket, 'PUT /api/v1/groups/:slug/membership/:uid'); + if (socket.uid <= 0) { throw new Error('[[error:invalid-uid]]'); }