From b4c0066737063f93b1865b09b48cb416b7a72fe6 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 4 Mar 2026 11:12:36 -0500 Subject: [PATCH] feat: new API route for removing privileges The old route was a DELETE method that took a request body. The body is sometimes stripped by some proxies, so this new method does not require a request body, and the member (to whom the privilege is revoked) is now in the route itself as a parameter. --- .../categories/cid/privileges/privilege.yaml | 7 +- .../cid/privileges/privilege/member.yaml | 125 ++++++++++++++++++ public/src/admin/manage/privileges.js | 11 +- src/controllers/write/categories.js | 4 +- src/routes/write/categories.js | 1 + 5 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 public/openapi/write/categories/cid/privileges/privilege/member.yaml diff --git a/public/openapi/write/categories/cid/privileges/privilege.yaml b/public/openapi/write/categories/cid/privileges/privilege.yaml index 9c7cba8882..bca5609917 100644 --- a/public/openapi/write/categories/cid/privileges/privilege.yaml +++ b/public/openapi/write/categories/cid/privileges/privilege.yaml @@ -136,8 +136,11 @@ put: delete: tags: - categories - summary: Rescinds category privilege for user/group - description: This operation rescinds a category privilege for a specific user or group + summary: Rescinds category privilege for user/group (Deprecated) + description: | + This method is **deprecated**, please use `DELETE /categories/:cid/:privilege/:member` instead when removing privileges via the API. + + This operation rescinds a category privilege for a specific user or group parameters: - in: path name: cid diff --git a/public/openapi/write/categories/cid/privileges/privilege/member.yaml b/public/openapi/write/categories/cid/privileges/privilege/member.yaml new file mode 100644 index 0000000000..65ea501407 --- /dev/null +++ b/public/openapi/write/categories/cid/privileges/privilege/member.yaml @@ -0,0 +1,125 @@ +delete: + tags: + - categories + summary: Rescinds category privilege for user/group + description: This operation rescinds a category privilege for a specific user or group + parameters: + - in: path + name: cid + schema: + type: string + required: true + description: a valid category id, `0` for global privileges, `admin` for admin privileges + example: 1 + - in: path + name: privilege + schema: + type: string + required: true + description: The specific privilege you would like to rescind. Privileges for groups must be prefixed `group:` + example: 'groups:ban' + responses: + '200': + description: Privilege successfully rescinded + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + labelData: + type: array + items: + type: object + properties: + label: + type: string + description: the name of the privilege displayed in the ACP dashboard + type: + type: string + description: type of the privilege (one of viewing, posting, moderation or other) + users: + type: array + items: + type: object + properties: + uid: + type: number + description: A user identifier + example: 1 + username: + type: string + description: A friendly name for a given user account + example: Dragon Fruit + displayname: + type: string + description: This is either username or fullname depending on forum and user settings + example: Dragon Fruit + picture: + type: string + description: A URL pointing to a picture to be used as the user's avatar + example: 'https://images.unsplash.com/photo-1560070094-e1f2ddec4337?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=256&h=256&q=80' + nullable: true + 'icon:text': + type: string + description: A single-letter representation of a username. This is used in the auto-generated icon given to users without an avatar + example: D + 'icon:bgColor': + type: string + description: A six-character hexadecimal colour code assigned to the user. This value is used in conjunction with `icon:text` for the user's auto-generated icon + example: '#9c27b0' + banned: + type: number + description: A Boolean representing whether a user is banned or not + example: 0 + banned_until_readable: + type: string + description: An ISO 8601 formatted date string representing the moment a ban will be lifted, or the words "Not Banned" + example: Not Banned + privileges: + type: object + additionalProperties: + description: A set of privileges with either true or false + groups: + type: array + items: + type: object + properties: + name: + type: string + nameEscaped: + type: string + privileges: + type: object + additionalProperties: + description: A set of privileges with either true or false + types: + type: object + description: Each privilege will have a key in this object, the value will be the type of the privilege (viewing, posting, moderation or other) + isPrivate: + type: boolean + isSystem: + type: boolean + keys: + type: object + properties: + users: + type: array + items: + type: string + description: "Privilege name" + groups: + type: array + items: + type: string + description: "Privilege name" + columnCountUserOther: + type: number + description: "The number of additional user privileges added by plugins" + columnCountGroupOther: + type: number + description: "The number of additional user privileges added by plugins" \ No newline at end of file diff --git a/public/src/admin/manage/privileges.js b/public/src/admin/manage/privileges.js index f5f3b689c1..e1084e2558 100644 --- a/public/src/admin/manage/privileges.js +++ b/public/src/admin/manage/privileges.js @@ -248,7 +248,16 @@ define('admin/manage/privileges', [ applyPrivilegesToColumn(inputSelectorFn, sourceChecked); }; - Privileges.setPrivilege = (member, privilege, state) => api[state ? 'put' : 'del'](`/categories/${isNaN(cid) ? 0 : cid}/privileges/${encodeURIComponent(privilege)}`, { member }); + Privileges.setPrivilege = (member, privilege, state) => { + const method = state ? 'put' : 'del'; + let url = `/categories/${isNaN(cid) ? 0 : cid}/privileges/${encodeURIComponent(privilege)}`; + const payload = { member }; + if (!state) { + url += `/${member}`; + delete payload.member; + } + return api[method](url, payload); + }; Privileges.addUserToPrivilegeTable = function () { const modal = bootbox.dialog({ diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js index 996a9d386a..07ac75794f 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -83,12 +83,12 @@ Categories.getPrivileges = async (req, res) => { }; Categories.setPrivilege = async (req, res) => { - const { cid, privilege } = req.params; + const { cid, privilege, member } = req.params; await api.categories.setPrivilege(req, { cid, privilege, - member: req.body.member, + member: member || req.body.member, set: req.method === 'PUT', }); diff --git a/src/routes/write/categories.js b/src/routes/write/categories.js index 066e75bb96..d9b065a99e 100644 --- a/src/routes/write/categories.js +++ b/src/routes/write/categories.js @@ -27,6 +27,7 @@ module.exports = function () { setupApiRoute(router, 'get', '/:cid/privileges', [...middlewares], controllers.write.categories.getPrivileges); setupApiRoute(router, 'put', '/:cid/privileges/:privilege', [...middlewares, middleware.checkRequired.bind(null, ['member'])], controllers.write.categories.setPrivilege); setupApiRoute(router, 'delete', '/:cid/privileges/:privilege', [...middlewares, middleware.checkRequired.bind(null, ['member'])], controllers.write.categories.setPrivilege); + setupApiRoute(router, 'delete', '/:cid/privileges/:privilege/:member', [...middlewares], controllers.write.categories.setPrivilege); setupApiRoute(router, 'put', '/:cid/moderator/:uid', [...middlewares, middleware.assert.category], controllers.write.categories.setModerator); setupApiRoute(router, 'delete', '/:cid/moderator/:uid', [...middlewares, middleware.assert.category], controllers.write.categories.setModerator);