diff --git a/public/openapi/components/schemas/SettingsObj.yaml b/public/openapi/components/schemas/SettingsObj.yaml new file mode 100644 index 000000000000..d7ecfc42aab4 --- /dev/null +++ b/public/openapi/components/schemas/SettingsObj.yaml @@ -0,0 +1,143 @@ +Settings: + type: object + properties: + showemail: + type: boolean + description: Show user email in profile page + usePagination: + type: boolean + description: Toggles between pagination (when enabled), or infinite scrolling (when disabled) + topicsPerPage: + type: number + description: Number of topics displayed on a category page + postsPerPage: + type: number + description: Number of posts displayed on a topic page + topicPostSort: + type: string + description: Default sorting strategy of the posts in of a topic + openOutgoingLinksInNewTab: + type: boolean + description: Whether to automatically open all external links in a new tab + dailyDigestFreq: + type: string + description: How often to receive the scheduled digest from this forum + showfullname: + type: boolean + description: Show user full name in profile page + followTopicsOnCreate: + type: boolean + description: Automatically be notified of new posts in a topic, when you create a topic + followTopicsOnReply: + type: boolean + description: + restrictChat: + type: boolean + description: + topicSearchEnabled: + type: boolean + description: + categoryTopicSort: + type: string + description: + userLang: + type: string + description: + bootswatchSkin: + type: string + description: + homePageRoute: + type: string + description: + scrollToMyPost: + type: boolean + description: + notificationType_new-chat: + type: string + description: + notificationType_new-reply: + type: string + description: + notificationType_post-edit: + type: string + description: + sendChatNotifications: + nullable: true + sendPostNotifications: + nullable: true + notificationType_upvote: + type: string + description: + notificationType_new-topic: + type: string + description: + notificationType_follow: + type: string + description: + notificationType_group-invite: + type: string + description: + upvoteNotifFreq: + type: string + description: + notificationType_mention: + type: string + description: + acpLang: + type: string + description: + notificationType_new-register: + type: string + description: + notificationType_post-queue: + type: string + description: + notificationType_new-post-flag: + type: string + description: + notificationType_new-user-flag: + type: string + description: + categoryWatchState: + type: string + description: + notificationType_group-request-membership: + type: string + description: + uid: + type: number + description: + description: A user identifier + required: + - showemail + - usePagination + - topicsPerPage + - postsPerPage + - topicPostSort + - openOutgoingLinksInNewTab + - dailyDigestFreq + - showfullname + - followTopicsOnCreate + - followTopicsOnReply + - restrictChat + - topicSearchEnabled + - categoryTopicSort + - userLang + - bootswatchSkin + - homePageRoute + - scrollToMyPost + - notificationType_new-chat + - notificationType_new-reply + - notificationType_upvote + - notificationType_new-topic + - notificationType_follow + - notificationType_group-invite + - upvoteNotifFreq + - acpLang + - notificationType_new-register + - notificationType_post-queue + - notificationType_new-post-flag + - notificationType_new-user-flag + - categoryWatchState + - notificationType_group-request-membership + - uid \ No newline at end of file diff --git a/public/openapi/read/user/userslug/settings.yaml b/public/openapi/read/user/userslug/settings.yaml index d4a6d3ebb8c3..e74a44fb2fc6 100644 --- a/public/openapi/read/user/userslug/settings.yaml +++ b/public/openapi/read/user/userslug/settings.yaml @@ -20,114 +20,7 @@ get: - type: object properties: settings: - type: object - properties: - showemail: - type: boolean - usePagination: - type: boolean - topicsPerPage: - type: number - postsPerPage: - type: number - topicPostSort: - type: string - openOutgoingLinksInNewTab: - type: boolean - dailyDigestFreq: - type: string - showfullname: - type: boolean - followTopicsOnCreate: - type: boolean - followTopicsOnReply: - type: boolean - restrictChat: - type: boolean - topicSearchEnabled: - type: boolean - categoryTopicSort: - type: string - userLang: - type: string - bootswatchSkin: - type: string - homePageRoute: - type: string - scrollToMyPost: - type: boolean - notificationType_new-chat: - type: string - notificationType_new-reply: - type: string - notificationType_post-edit: - type: string - sendChatNotifications: - nullable: true - sendPostNotifications: - nullable: true - notificationType_upvote: - type: string - notificationType_new-topic: - type: string - notificationType_follow: - type: string - notificationType_group-invite: - type: string - upvoteNotifFreq: - type: string - notificationType_mention: - type: string - acpLang: - type: string - notificationType_new-register: - type: string - notificationType_post-queue: - type: string - notificationType_new-post-flag: - type: string - notificationType_new-user-flag: - type: string - categoryWatchState: - type: string - notificationType_group-request-membership: - type: string - uid: - type: number - description: A user identifier - required: - - showemail - - usePagination - - topicsPerPage - - postsPerPage - - topicPostSort - - openOutgoingLinksInNewTab - - dailyDigestFreq - - showfullname - - followTopicsOnCreate - - followTopicsOnReply - - restrictChat - - topicSearchEnabled - - categoryTopicSort - - userLang - - bootswatchSkin - - homePageRoute - - scrollToMyPost - - notificationType_new-chat - - notificationType_new-reply - - notificationType_upvote - - notificationType_new-topic - - notificationType_follow - - notificationType_group-invite - - upvoteNotifFreq - - acpLang - - notificationType_new-register - - notificationType_post-queue - - notificationType_new-post-flag - - notificationType_new-user-flag - - categoryWatchState - - notificationType_group-request-membership - - uid + $ref: ../../../components/schemas/SettingsObj.yaml#/Settings languages: type: array items: diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 75cc5a2ad0f1..982a26a8c1d5 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -30,6 +30,10 @@ tags: paths: /users/{uid}: $ref: 'write/users/uid.yaml' + /users/{uid}/settings: + $ref: 'write/users/uid/settings.yaml' + /users/{uid}/settings/{setting}: + $ref: 'write/users/uid/settings/setting.yaml' /users/{uid}/password: $ref: 'write/users/uid/password.yaml' /users/{uid}/follow: diff --git a/public/openapi/write/users/uid/settings.yaml b/public/openapi/write/users/uid/settings.yaml new file mode 100644 index 000000000000..193f56f8a0b7 --- /dev/null +++ b/public/openapi/write/users/uid/settings.yaml @@ -0,0 +1,35 @@ +put: + tags: + - users + summary: update user settings + parameters: + - in: path + name: uid + schema: + type: integer + required: true + description: uid of the user + requestBody: + content: + application/json: + schema: + type: object + properties: + settings: + type: object + description: An object containing key-value pairs of user settings to update + example: + showemail: '0' + showfullname: '1' + responses: + '200': + description: successfully updated user settings + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + $ref: ../../../components/schemas/SettingsObj.yaml#/Settings \ No newline at end of file diff --git a/public/openapi/write/users/uid/settings/setting.yaml b/public/openapi/write/users/uid/settings/setting.yaml new file mode 100644 index 000000000000..455eb787dda8 --- /dev/null +++ b/public/openapi/write/users/uid/settings/setting.yaml @@ -0,0 +1,40 @@ +put: + tags: + - users + summary: update one user setting + parameters: + - in: path + name: uid + schema: + type: integer + required: true + description: uid of the user + example: '1' + - in: path + name: setting + schema: + type: string + required: true + description: name of the setting you wish to update + example: 'showemail' + requestBody: + content: + application/json: + schema: + type: object + properties: + value: + type: string + example: '1' + responses: + '200': + description: successfully updated user settings + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../../components/schemas/Status.yaml#/Status + response: + type: object \ No newline at end of file diff --git a/public/src/client/account/settings.js b/public/src/client/account/settings.js index 39a2bfce1804..10b274242da7 100644 --- a/public/src/client/account/settings.js +++ b/public/src/client/account/settings.js @@ -1,7 +1,7 @@ 'use strict'; -define('forum/account/settings', ['forum/account/header', 'components', 'translator'], function (header, components, translator) { +define('forum/account/settings', ['forum/account/header', 'components', 'translator', 'api'], function (header, components, translator, api) { var AccountSettings = {}; // If page skin is changed but not saved, switch the skin back @@ -67,11 +67,7 @@ define('forum/account/settings', ['forum/account/header', 'components', 'transla } function saveSettings(settings) { - socket.emit('user.saveSettings', { uid: ajaxify.data.theirid, settings: settings }, function (err, newSettings) { - if (err) { - return app.alertError(err.message); - } - + api.put(`/users/${ajaxify.data.uid}/settings`, { settings }).then((newSettings) => { app.alertSuccess('[[success:settings-saved]]'); var languageChanged = false; for (var key in newSettings) { diff --git a/public/src/client/category.js b/public/src/client/category.js index b9fa983f6721..5a9c27a338c6 100644 --- a/public/src/client/category.js +++ b/public/src/client/category.js @@ -24,7 +24,7 @@ define('forum/category', [ topicList.init('category', loadTopicsAfter); - sort.handleSort('categoryTopicSort', 'user.setCategorySort', 'category/' + ajaxify.data.slug); + sort.handleSort('categoryTopicSort', 'setCategorySort', 'category/' + ajaxify.data.slug); if (!config.usePagination) { navigator.init('[component="category/topic"]', ajaxify.data.topic_count, Category.toTop, Category.toBottom, Category.navigatorCallback); diff --git a/public/src/client/topic.js b/public/src/client/topic.js index 9b0e0d062100..df49490c64a1 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -49,7 +49,7 @@ define('forum/topic', [ threadTools.init(tid, $('.topic')); events.init(); - sort.handleSort('topicPostSort', 'user.setTopicSort', 'topic/' + ajaxify.data.slug); + sort.handleSort('topicPostSort', 'setTopicSort', 'topic/' + ajaxify.data.slug); if (!config.usePagination) { infinitescroll.init($('[component="topic"]'), posts.loadMorePosts); diff --git a/public/src/modules/sort.js b/public/src/modules/sort.js index abc15f2765e9..9d718788442e 100644 --- a/public/src/modules/sort.js +++ b/public/src/modules/sort.js @@ -1,10 +1,10 @@ 'use strict'; -define('sort', ['components'], function (components) { +define('sort', ['components', 'api'], function (components, api) { var module = {}; - module.handleSort = function (field, method, gotoOnSave) { + module.handleSort = function (field, key, gotoOnSave) { var threadSort = components.get('thread/sort'); threadSort.find('i').removeClass('fa-check'); var currentSetting = threadSort.find('a[data-sort="' + config[field] + '"]'); @@ -20,10 +20,9 @@ define('sort', ['components'], function (components) { } var newSetting = $(this).attr('data-sort'); if (app.user.uid) { - socket.emit(method, newSetting, function (err) { - if (err) { - return app.alertError(err.message); - } + api.put(`/users/${app.user.uid}/settings/${key}`, { + value: newSetting, + }).then(() => { refresh(newSetting, utils.params()); }); } else { diff --git a/src/api/users.js b/src/api/users.js index 0a8743068171..e6d523c36c9c 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -89,6 +89,23 @@ usersAPI.deleteMany = async function (caller, data) { } }; +usersAPI.updateSettings = async function (caller, data) { + if (!caller.uid || !data || !data.settings) { + throw new Error('[[error:invalid-data]]'); + } + + const canEdit = await privileges.users.canEdit(caller.uid, data.uid); + if (!canEdit) { + throw new Error('[[error:no-privileges]]'); + } + + return await user.saveSettings(data.uid, data.settings); +}; + +usersAPI.updateSetting = async function (caller, data) { + await user.setSetting(data.uid, data.setting, data.value); +}; + usersAPI.changePassword = async function (caller, data) { await user.changePassword(caller.uid, Object.assign(data, { ip: caller.ip })); await events.log({ diff --git a/src/controllers/write/users.js b/src/controllers/write/users.js index c76a6d0b5bf7..febdd1b485d3 100644 --- a/src/controllers/write/users.js +++ b/src/controllers/write/users.js @@ -28,6 +28,16 @@ Users.deleteMany = async (req, res) => { helpers.formatApiResponse(200, res); }; +Users.updateSettings = async (req, res) => { + const settings = await api.users.updateSettings(req, { ...req.body, uid: req.params.uid }); + helpers.formatApiResponse(200, res, settings); +}; + +Users.updateSetting = async (req, res) => { + await api.users.updateSetting(req, { ...req.params, value: req.body.value }); + helpers.formatApiResponse(200, res); +}; + Users.changePassword = async (req, res) => { await api.users.changePassword(req, { ...req.body, uid: req.params.uid }); helpers.formatApiResponse(200, res); diff --git a/src/routes/write/users.js b/src/routes/write/users.js index 55722d4f61ec..92428f237d22 100644 --- a/src/routes/write/users.js +++ b/src/routes/write/users.js @@ -21,6 +21,9 @@ function authenticatedRoutes() { setupApiRoute(router, 'put', '/:uid', [...middlewares, middleware.assert.user], controllers.write.users.update); setupApiRoute(router, 'delete', '/:uid', [...middlewares, middleware.assert.user, middleware.exposePrivileges], controllers.write.users.delete); + setupApiRoute(router, 'put', '/:uid/settings', [...middlewares, middleware.checkRequired.bind(null, ['settings'])], controllers.write.users.updateSettings); + setupApiRoute(router, 'put', '/:uid/settings/:setting', [...middlewares, middleware.checkRequired.bind(null, ['value'])], controllers.write.users.updateSetting); + setupApiRoute(router, 'put', '/:uid/password', [...middlewares, middleware.checkRequired.bind(null, ['newPassword']), middleware.assert.user], controllers.write.users.changePassword); setupApiRoute(router, 'put', '/:uid/follow', [...middlewares, middleware.assert.user], controllers.write.users.follow); diff --git a/src/socket.io/user.js b/src/socket.io/user.js index 9080e342569a..4e5f9b06d103 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -168,22 +168,27 @@ SocketUser.unfollow = async function (socket, data) { }; SocketUser.saveSettings = async function (socket, data) { - if (!socket.uid || !data || !data.settings) { - throw new Error('[[error:invalid-data]]'); - } - const canEdit = await privileges.users.canEdit(socket.uid, data.uid); - if (!canEdit) { - throw new Error('[[error:no-privileges]]'); - } - return await user.saveSettings(data.uid, data.settings); + sockets.warnDeprecated(socket, 'PUT /api/v3/users/:uid/settings'); + const settings = await api.users.updateSettings(socket, data); + return settings; }; SocketUser.setTopicSort = async function (socket, sort) { - await user.setSetting(socket.uid, 'topicPostSort', sort); + sockets.warnDeprecated(socket, 'PUT /api/v3/users/:uid/setting/topicPostSort'); + await api.users.updateSetting(socket, { + uid: socket.uid, + setting: 'topicPostSort', + value: sort, + }); }; SocketUser.setCategorySort = async function (socket, sort) { - await user.setSetting(socket.uid, 'categoryTopicSort', sort); + sockets.warnDeprecated(socket, 'PUT /api/v3/users/:uid/setting/categoryTopicSort'); + await api.users.updateSetting(socket, { + uid: socket.uid, + setting: 'categoryTopicSort', + value: sort, + }); }; SocketUser.getUnreadCount = async function (socket) {