From ba345e53e86d2d97fa8dd0e4495719c15330d963 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 28 Apr 2020 12:07:04 -0400 Subject: [PATCH] feat(writeapi): added POST /api/v1/groups --- .../components/schemas/GroupObject.yaml | 2 +- public/openapi/write.yaml | 55 ++++++++++++++ public/src/admin/manage/groups.js | 28 ++++---- public/src/client/groups/list.js | 18 ++--- src/controllers/write/groups.js | 36 ++++++++++ src/groups/create.js | 5 +- src/middleware/expose.js | 7 ++ src/routes/write/groups.js | 72 +++++++++++++++++++ src/socket.io/admin/groups.js | 3 + src/socket.io/groups.js | 3 + 10 files changed, 204 insertions(+), 25 deletions(-) create mode 100644 src/controllers/write/groups.js create mode 100644 src/routes/write/groups.js diff --git a/public/openapi/components/schemas/GroupObject.yaml b/public/openapi/components/schemas/GroupObject.yaml index 89b4cf9a4aa4..e9802cdf7e67 100644 --- a/public/openapi/components/schemas/GroupObject.yaml +++ b/public/openapi/components/schemas/GroupObject.yaml @@ -77,7 +77,7 @@ GroupFullObject: nullable: true GroupDataObject: type: object - description: The response from an internal call to `Groups.getGroupData(, [])` with **explicitly** no fields passed in + description: The response from an internal call to `Groups.getGroupsFields(, [])` with **explicitly** no fields passed in properties: name: type: string diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 65248c63e75b..709bfecdef99 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -364,6 +364,61 @@ paths: $ref: '#/components/schemas/Status' response: $ref: '#/components/schemas/CategoryObj' + /groups/: + post: + tags: + - groups + summary: Create a new group + description: This operation creates a new group + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + timestamp: + type: number + disableJoinRequests: + type: number + enum: [0, 1] + disableLeave: + type: number + enum: [0, 1] + hidden: + type: number + enum: [0, 1] + ownerUid: + type: number + private: + type: number + enum: [0, 1] + description: + type: string + userTitleEnabled: + type: number + enum: [0, 1] + createtime: + type: number + required: + - name + example: + name: 'My Test Group' + hidden: 1 + responses: + '200': + description: group successfully created + content: + application/json: + schema: + type: object + properties: + status: + $ref: '#/components/schemas/Status' + response: + $ref: components/schemas/GroupObject.yaml#/GroupDataObject components: schemas: Status: diff --git a/public/src/admin/manage/groups.js b/public/src/admin/manage/groups.js index a9cea81cbb5f..42c517c08c9b 100644 --- a/public/src/admin/manage/groups.js +++ b/public/src/admin/manage/groups.js @@ -37,20 +37,22 @@ define('admin/manage/groups', [ hidden: $('#create-group-hidden').is(':checked') ? 1 : 0, }; - socket.emit('admin.groups.create', submitObj, function (err, groupData) { - if (err) { - if (err.hasOwnProperty('message') && utils.hasLanguageKey(err.message)) { - err = '[[admin/manage/groups:alerts.create-failure]]'; - } - createModalError.translateHtml(err).removeClass('hide'); - } else { - createModalError.addClass('hide'); - createGroupName.val(''); - createModal.on('hidden.bs.modal', function () { - ajaxify.go('admin/manage/groups/' + groupData.name); - }); - createModal.modal('hide'); + $.ajax({ + url: config.relative_path + '/api/v1/groups', + method: 'post', + data: submitObj, + }).done(function (res) { + createModalError.addClass('hide'); + createGroupName.val(''); + createModal.on('hidden.bs.modal', function () { + ajaxify.go('admin/manage/groups/' + res.response.name); + }); + createModal.modal('hide'); + }).fail(function (ev) { + if (utils.hasLanguageKey(ev.responseJSON.status.message)) { + ev.responseJSON.status.message = '[[admin/manage/groups:alerts.create-failure]]'; } + createModalError.translateHtml(ev.responseJSON.status.message).removeClass('hide'); }); }); diff --git a/public/src/client/groups/list.js b/public/src/client/groups/list.js index d263639b8508..0d7a7da4d3b2 100644 --- a/public/src/client/groups/list.js +++ b/public/src/client/groups/list.js @@ -11,14 +11,16 @@ define('forum/groups/list', ['forum/infinitescroll', 'benchpress'], function (in $('button[data-action="new"]').on('click', function () { bootbox.prompt('[[groups:new-group.group_name]]', function (name) { if (name && name.length) { - socket.emit('groups.create', { - name: name, - }, function (err) { - if (!err) { - ajaxify.go('groups/' + utils.slugify(name)); - } else { - app.alertError(err.message); - } + $.ajax({ + url: config.relative_path + '/api/v1/groups', + method: 'post', + data: { + name: name, + }, + }).done(function (res) { + ajaxify.go('groups/' + res.response.slug); + }).fail(function (ev) { + app.alertError(ev.responseJSON.status.message); }); } }); diff --git a/src/controllers/write/groups.js b/src/controllers/write/groups.js new file mode 100644 index 000000000000..f253da51b37b --- /dev/null +++ b/src/controllers/write/groups.js @@ -0,0 +1,36 @@ +'use strict'; + +const groups = require('../../groups'); +const events = require('../../events'); + +const helpers = require('../helpers'); + +const Groups = module.exports; + +Groups.create = async (req, res) => { + if (typeof req.body.name !== 'string' || groups.isPrivilegeGroup(req.body.name)) { + throw new Error('[[error:invalid-group-name]]'); + } + + if (!res.locals.privileges['group:create']) { + throw new Error('[[error:no-privileges]]'); + } + + req.body.ownerUid = req.user.uid; + req.body.system = false; + + const groupObj = await groups.create(req.body); + helpers.formatApiResponse(200, res, groupObj); + logGroupEvent(req, 'group-create', { + groupName: req.body.name, + }); +}; + +function logGroupEvent(req, event, additional) { + events.log({ + type: event, + uid: req.user.uid, + ip: req.ip, + ...additional, + }); +} diff --git a/src/groups/create.js b/src/groups/create.js index 76aec0afe094..1bc7ba139a36 100644 --- a/src/groups/create.js +++ b/src/groups/create.js @@ -25,7 +25,7 @@ module.exports = function (Groups) { const memberCount = data.hasOwnProperty('ownerUid') ? 1 : 0; const isPrivate = data.hasOwnProperty('private') && data.private !== undefined ? parseInt(data.private, 10) === 1 : true; - const groupData = { + let groupData = { name: data.name, slug: utils.slugify(data.name), createtime: timestamp, @@ -48,8 +48,6 @@ module.exports = function (Groups) { if (data.hasOwnProperty('ownerUid')) { await db.setAdd('group:' + groupData.name + ':owners', data.ownerUid); await db.sortedSetAdd('group:' + groupData.name + ':members', timestamp, data.ownerUid); - - groupData.ownerUid = data.ownerUid; } if (!isHidden && !isSystem) { @@ -62,6 +60,7 @@ module.exports = function (Groups) { await db.setObjectField('groupslug:groupname', groupData.slug, groupData.name); + groupData = await Groups.getGroupData(groupData.name); plugins.fireHook('action:group.create', { group: groupData }); return groupData; }; diff --git a/src/middleware/expose.js b/src/middleware/expose.js index be4a7227127f..65d096418646 100644 --- a/src/middleware/expose.js +++ b/src/middleware/expose.js @@ -6,6 +6,7 @@ */ const user = require('../user'); +const privileges = require('../privileges'); const utils = require('../utils'); module.exports = function (middleware) { @@ -37,4 +38,10 @@ module.exports = function (middleware) { res.locals.privileges = hash; return next(); }; + + middleware.exposePrivilegeSet = async (req, res, next) => { + // Exposes a user's global privilege set + res.locals.privileges = await privileges.global.get(req.user.uid); + return next(); + }; }; diff --git a/src/routes/write/groups.js b/src/routes/write/groups.js new file mode 100644 index 000000000000..0811a3803801 --- /dev/null +++ b/src/routes/write/groups.js @@ -0,0 +1,72 @@ +'use strict'; + +const router = require('express').Router(); +const middleware = require('../../middleware'); +const controllers = require('../../controllers'); +const routeHelpers = require('../helpers'); + +const setupApiRoute = routeHelpers.setupApiRoute; + +module.exports = function () { + const middlewares = [middleware.authenticate]; + + setupApiRoute(router, '/', middleware, [...middlewares, middleware.checkRequired.bind(null, ['name']), middleware.exposePrivilegeSet], 'post', controllers.write.groups.create); + + // 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); + // }); + // } + // }); + + // 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) { + // errorHandler.handle(err, res); + // }); + // }); + + // app.delete('/:slug/membership', apiMiddleware.requireUser, middleware.exposeGroupName, apiMiddleware.validateGroup, function(req, res) { + // Groups.isMember(req.user.uid, res.locals.groupName, function(err, isMember) { + // if (isMember) { + // Groups.leave(res.locals.groupName, req.user.uid, function(err) { + // errorHandler.handle(err, res); + // }); + // } else { + // errorHandler.respond(400, res); + // } + // }); + // }); + + // app.delete('/:slug/membership/:uid', middleware.exposeGroupName, apiMiddleware.validateGroup, apiMiddleware.requireUser, apiMiddleware.requireAdmin, function(req, res) { + // Groups.isMember(req.params.uid, res.locals.groupName, function(err, isMember) { + // if (isMember) { + // Groups.leave(res.locals.groupName, req.params.uid, function(err) { + // errorHandler.handle(err, res); + // }); + // } else { + // errorHandler.respond(400, res); + // } + // }); + // }); + + return router; +}; diff --git a/src/socket.io/admin/groups.js b/src/socket.io/admin/groups.js index b58bc81e03e6..5cb14d9d5013 100644 --- a/src/socket.io/admin/groups.js +++ b/src/socket.io/admin/groups.js @@ -1,10 +1,13 @@ 'use strict'; const groups = require('../../groups'); +const sockets = require('..'); const Groups = module.exports; Groups.create = async function (socket, data) { + sockets.warnDeprecated(socket, 'POST /api/v1/groups'); + if (!data) { throw new Error('[[error:invalid-data]]'); } else if (groups.isPrivilegeGroup(data.name)) { diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index f6957a31f790..e1b7dcb4a8af 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -8,6 +8,7 @@ const utils = require('../utils'); const events = require('../events'); const privileges = require('../privileges'); const notifications = require('../notifications'); +const sockets = require('.'); const SocketGroups = module.exports; @@ -277,6 +278,8 @@ SocketGroups.kick = async (socket, data) => { }; SocketGroups.create = async (socket, data) => { + sockets.warnDeprecated(socket, 'POST /api/v1/groups'); + if (!socket.uid) { throw new Error('[[error:no-privileges]]'); } else if (typeof data.name !== 'string' || groups.isPrivilegeGroup(data.name)) {