From 3104e6d11c43492c72dd63ccd6566a9153701fe2 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 10 Aug 2017 13:39:33 +0200 Subject: [PATCH 1/4] fixes: U4-10243 Umbraco 7.7. Difference between "Enable/Disable" and "LockedOut" --- .../src/common/resources/users.resource.js | 17 +++++++ .../src/views/users/user.controller.js | 37 +++++++++----- .../src/views/users/user.html | 48 ++++++++++++------- .../users/views/users/users.controller.js | 34 +++++++++++++ .../src/views/users/views/users/users.html | 11 +++++ .../umbraco/config/lang/en_us.xml | 5 ++ src/Umbraco.Web/Editors/UsersController.cs | 23 +++++++++ 7 files changed, 147 insertions(+), 28 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index 0acd30afe0..55a8636bdd 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -55,6 +55,22 @@ 'Failed to enable the users ' + userIds.join(",")); } + function unlockUsers(userIds) { + if (!userIds) { + throw "userIds not specified"; + } + + //we need to create a custom query string for the usergroup array, so create it now and we can append the user groups if needed + var qry = "userIds=" + userIds.join("&userIds="); + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "PostUnlockUsers", qry)), + 'Failed to enable the users ' + userIds.join(",")); + } + function setUserGroupsOnUsers(userGroups, userIds) { var userGroupAliases = userGroups.map(function(o) { return o.alias; }); var query = "userGroupAliases=" + userGroupAliases.join("&userGroupAliases=") + "&userIds=" + userIds.join("&userIds="); @@ -185,6 +201,7 @@ var resource = { disableUsers: disableUsers, enableUsers: enableUsers, + unlockUsers: unlockUsers, setUserGroupsOnUsers: setUserGroupsOnUsers, getPagedResults: getPagedResults, getUser: getUser, diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index 237c44255c..21e83346ec 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -30,6 +30,7 @@ vm.removeSelectedItem = removeSelectedItem; vm.disableUser = disableUser; vm.enableUser = enableUser; + vm.unlockUser = unlockUser; vm.clearAvatar = clearAvatar; vm.save = save; vm.toggleChangePassword = toggleChangePassword; @@ -234,11 +235,11 @@ function disableUser() { vm.disableUserButtonState = "busy"; usersResource.disableUsers([vm.user.id]).then(function (data) { - vm.user.userState = 1; - setUserDisplayState(); - vm.disableUserButtonState = "success"; - formHelper.showNotifications(data); - }, function(error){ + vm.user.userState = 1; + setUserDisplayState(); + vm.disableUserButtonState = "success"; + formHelper.showNotifications(data); + }, function (error) { vm.disableUserButtonState = "error"; formHelper.showNotifications(error.data); }); @@ -247,16 +248,28 @@ function enableUser() { vm.enableUserButtonState = "busy"; usersResource.enableUsers([vm.user.id]).then(function (data) { - vm.user.userState = 0; - setUserDisplayState(); - vm.enableUserButtonState = "success"; - formHelper.showNotifications(data); - }, function(error){ - vm.disableUserButtonState = "error"; + vm.user.userState = 0; + setUserDisplayState(); + vm.enableUserButtonState = "success"; + formHelper.showNotifications(data); + }, function (error) { + vm.enableUserButtonState = "error"; + formHelper.showNotifications(error.data); + }); + } + + function unlockUser() { + vm.unlockUserButtonState = "busy"; + usersResource.unlockUsers([vm.user.id]).then(function (data) { + vm.user.userState = 0; + setUserDisplayState(); + vm.unlockUserButtonState = "success"; + formHelper.showNotifications(data); + }, function (error) { + vm.unlockUserButtonState = "error"; formHelper.showNotifications(error.data); }); } - function clearAvatar() { // get user diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index 84e50a3360..7d38936a2f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -1,6 +1,8 @@
- + +
@@ -233,29 +235,43 @@
-
- - -
+ size="s"> + +
+ +
+ + +
+ +
+
@@ -267,7 +283,7 @@ label-key="general_changePassword" state="changePasswordButtonState" ng-if="vm.changePasswordModel.isChanging === false" - size="m"> + size="s"> diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 96db5e07b1..5a0b6e60b0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -28,6 +28,7 @@ vm.allowDisableUser = true; vm.allowEnableUser = true; + vm.allowUnlockUser = true; vm.allowSetUserGroup = true; vm.layouts = [ @@ -76,6 +77,7 @@ vm.clickUser = clickUser; vm.disableUsers = disableUsers; vm.enableUsers = enableUsers; + vm.unlockUsers = unlockUsers; vm.openBulkUserGroupPicker = openBulkUserGroupPicker; vm.openUserGroupPicker = openUserGroupPicker; vm.removeSelectedUserGroup = removeSelectedUserGroup; @@ -241,6 +243,28 @@ }); } + function unlockUsers() { + vm.unlockUserButtonState = "busy"; + usersResource.unlockUsers(vm.selection).then(function (data) { + // update userState + angular.forEach(vm.selection, function (userId) { + var user = getUserFromArrayById(userId, vm.users); + if (user) { + user.userState = 0; + } + }); + // show the correct badges + setUserDisplayState(vm.users); + // show notification + formHelper.showNotifications(data); + vm.unlockUserButtonState = "init"; + clearSelection(); + }, function (error) { + vm.unlockUserButtonState = "error"; + formHelper.showNotifications(error.data); + }); + } + function getUserFromArrayById(userId, users) { return _.find(users, function (u) { return u.id === userId }); } @@ -549,6 +573,7 @@ // reset all states vm.allowDisableUser = true; vm.allowEnableUser = true; + vm.allowUnlockUser = true; vm.allowSetUserGroup = true; var firstSelectedUserGroups; @@ -563,6 +588,7 @@ if (user.isCurrentUser) { vm.allowDisableUser = false; vm.allowEnableUser = false; + vm.allowUnlockUser = false; vm.allowSetUserGroup = false; return; } @@ -579,6 +605,14 @@ vm.allowEnableUser = false; } + if (user.userDisplayState && user.userDisplayState.key === "LockedOut") { + vm.allowEnableUser = false; + } + + if (user.userDisplayState && user.userDisplayState.key !== "LockedOut") { + vm.allowUnlockUser = false; + } + // store the user group aliases of the first selected user if (!firstSelectedUserGroups) { firstSelectedUserGroups = user.userGroups.map(function (ug) { return ug.alias; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index c938329926..3c5ef78c8a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -81,6 +81,17 @@ action="vm.enableUsers()">
+
+ + +
Translate Update Set permissions + Unlock Content @@ -1149,6 +1150,10 @@ To manage your website, simply open the Umbraco back office and start adding con User groups have been set Deleted %0% user groups %0% was deleted + Unlocked %0% users + An error occurred while unlocking the users + %0% is now unlocked + An error occurred while unlocking the user Uses CSS syntax ex: h1, .redHeader, .blueTex diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 236c669db4..51864e4323 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -557,6 +557,29 @@ namespace Umbraco.Web.Editors return Request.CreateNotificationSuccessResponse( Services.TextService.Localize("speechBubbles/enableUserSuccess", new[] { users[0].Name })); + } + + /// + /// Unlocks the users with the given user ids + /// + /// + public HttpResponseMessage PostUnlockUsers([FromUri]int[] userIds) + { + var users = Services.UserService.GetUsersById(userIds).ToArray(); + foreach (var u in users) + { + u.IsLockedOut = false; + } + Services.UserService.Save(users); + + if (users.Length > 1) + { + return Request.CreateNotificationSuccessResponse( + Services.TextService.Localize("speechBubbles/unlockUsersSuccess", new[] { userIds.Length.ToString() })); + } + + return Request.CreateNotificationSuccessResponse( + Services.TextService.Localize("speechBubbles/unlockUserSuccess", new[] { users[0].Name })); } public HttpResponseMessage PostSetUserGroupsOnUsers([FromUri]string[] userGroupAliases, [FromUri]int[] userIds) From 979a0c9e2526488bd531eda6f3e2c7ec01c8758a Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 10 Aug 2017 13:40:56 +0200 Subject: [PATCH 2/4] remove capitalise from badges --- src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less index 6998afa556..522b7564c1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less @@ -9,7 +9,6 @@ display: inline-flex; align-items: center; justify-content: center; - text-transform: capitalize; } // Colors From b607c9cd87330fc91eaf84155abca084ea82147d Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 10 Aug 2017 16:14:11 +0200 Subject: [PATCH 3/4] add documentation to users.resource --- .../src/common/resources/users.resource.js | 211 +++++++++++++++++- 1 file changed, 210 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index 55a8636bdd..72564398c0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -4,13 +4,33 @@ * @function * * @description - * Used by the users section to get users and send requests to create, invite, delete, etc. users. + * Used by the users section to get users and send requests to create, invite, disable, etc. users. */ (function () { 'use strict'; function usersResource($http, umbRequestHelper, $q, umbDataFormatter) { + /** + * @ngdoc method + * @name umbraco.resources.usersResource#clearAvatar + * @methodOf umbraco.resources.usersResource + * + * @description + * Deletes the user avatar + * + * ##usage + *
+          * usersResource.clearAvatar(1)
+          *    .then(function() {
+          *        alert("avatar is gone");
+          *    });
+          * 
+ * + * @param {Array} id id of user. + * @returns {Promise} resourcePromise object. + * + */ function clearAvatar(userId) { return umbRequestHelper.resourcePromise( @@ -22,6 +42,26 @@ 'Failed to clear the user avatar ' + userId); } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#disableUsers + * @methodOf umbraco.resources.usersResource + * + * @description + * Disables a collection of users + * + * ##usage + *
+          * usersResource.disableUsers([1, 2, 3, 4, 5])
+          *    .then(function() {
+          *        alert("users were disabled");
+          *    });
+          * 
+ * + * @param {Array} ids ids of users to disable. + * @returns {Promise} resourcePromise object. + * + */ function disableUsers(userIds) { if (!userIds) { throw "userIds not specified"; @@ -39,6 +79,26 @@ 'Failed to disable the users ' + userIds.join(",")); } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#enableUsers + * @methodOf umbraco.resources.usersResource + * + * @description + * Enables a collection of users + * + * ##usage + *
+          * usersResource.enableUsers([1, 2, 3, 4, 5])
+          *    .then(function() {
+          *        alert("users were enabled");
+          *    });
+          * 
+ * + * @param {Array} ids ids of users to enable. + * @returns {Promise} resourcePromise object. + * + */ function enableUsers(userIds) { if (!userIds) { throw "userIds not specified"; @@ -55,6 +115,26 @@ 'Failed to enable the users ' + userIds.join(",")); } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#unlockUsers + * @methodOf umbraco.resources.usersResource + * + * @description + * Unlocks a collection of users + * + * ##usage + *
+          * usersResource.unlockUsers([1, 2, 3, 4, 5])
+          *    .then(function() {
+          *        alert("users were unlocked");
+          *    });
+          * 
+ * + * @param {Array} ids ids of users to unlock. + * @returns {Promise} resourcePromise object. + * + */ function unlockUsers(userIds) { if (!userIds) { throw "userIds not specified"; @@ -71,6 +151,27 @@ 'Failed to enable the users ' + userIds.join(",")); } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#setUserGroupsOnUsers + * @methodOf umbraco.resources.usersResource + * + * @description + * Overwrites the existing user groups on a collection of users + * + * ##usage + *
+          * usersResource.setUserGroupsOnUsers(['admin', 'editor'], [1, 2, 3, 4, 5])
+          *    .then(function() {
+          *        alert("users were updated");
+          *    });
+          * 
+ * + * @param {Array} userGroupAliases aliases of user groups. + * @param {Array} ids ids of users to update. + * @returns {Promise} resourcePromise object. + * + */ function setUserGroupsOnUsers(userGroups, userIds) { var userGroupAliases = userGroups.map(function(o) { return o.alias; }); var query = "userGroupAliases=" + userGroupAliases.join("&userGroupAliases=") + "&userIds=" + userIds.join("&userIds="); @@ -83,6 +184,34 @@ 'Failed to set user groups ' + userGroupAliases.join(",") + ' on the users ' + userIds.join(",")); } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#getPagedResults + * @methodOf umbraco.resources.usersResource + * + * @description + * Get users + * + * ##usage + *
+          * usersResource.getPagedResults({pageSize: 10, pageNumber: 2})
+          *    .then(function(data) {
+          *        var users = data.items;
+          *        alert('they are here!');
+          *    });
+          * 
+ * + * @param {Object} options optional options object + * @param {Int} options.pageSize if paging data, number of users per page, default = 25 + * @param {Int} options.pageNumber if paging data, current page index, default = 1 + * @param {String} options.filter if provided, query will only return those with names matching the filter + * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` + * @param {String} options.orderBy property to order users by, default: `Username` + * @param {Array} options.userGroups property to filter users by user group + * @param {Array} options.userStates property to filter users by user state + * @returns {Promise} resourcePromise object containing an array of content items. + * + */ function getPagedResults(options) { var defaults = { pageSize: 25, @@ -135,6 +264,26 @@ 'Failed to retrieve users paged result'); } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#getUser + * @methodOf umbraco.resources.usersResource + * + * @description + * Gets a user + * + * ##usage + *
+          * usersResource.getUser(1)
+          *    .then(function(user) {
+          *        alert("It's here");
+          *    });
+          * 
+ * + * @param {Array} id user id. + * @returns {Promise} resourcePromise object containing the user. + * + */ function getUser(userId) { return umbRequestHelper.resourcePromise( @@ -146,6 +295,26 @@ "Failed to retrieve data for user " + userId); } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#createUser + * @methodOf umbraco.resources.usersResource + * + * @description + * Creates a new user + * + * ##usage + *
+          * usersResource.createUser(user)
+          *    .then(function(newUser) {
+          *        alert("It's here");
+          *    });
+          * 
+ * + * @param {Object} user user to create + * @returns {Promise} resourcePromise object containing the new user. + * + */ function createUser(user) { if (!user) { throw "user not specified"; @@ -163,6 +332,26 @@ "Failed to save user"); } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#inviteUser + * @methodOf umbraco.resources.usersResource + * + * @description + * Creates and sends an email invitation to a new user + * + * ##usage + *
+          * usersResource.inviteUser(user)
+          *    .then(function(newUser) {
+          *        alert("It's here");
+          *    });
+          * 
+ * + * @param {Object} user user to invite + * @returns {Promise} resourcePromise object containing the new user. + * + */ function inviteUser(user) { if (!user) { throw "user not specified"; @@ -180,6 +369,26 @@ "Failed to invite user"); } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#saveUser + * @methodOf umbraco.resources.usersResource + * + * @description + * Saves a user + * + * ##usage + *
+          * usersResource.saveUser(user)
+          *    .then(function(updatedUser) {
+          *        alert("It's here");
+          *    });
+          * 
+ * + * @param {Object} user object to save + * @returns {Promise} resourcePromise object containing the updated user. + * + */ function saveUser(user) { if (!user) { throw "user not specified"; From 1de79cf47304de4aa7cac0a358a409f6a475349d Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 15 Aug 2017 12:31:32 +1000 Subject: [PATCH 4/4] updated to use ASPNET Identity APIs for managing lockouts --- .../Security/BackOfficeUserStore.cs | 3 ++ src/Umbraco.Web/Editors/UsersController.cs | 33 +++++++++++++------ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 9e8d5c6170..322e1a2f86 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -545,6 +545,9 @@ namespace Umbraco.Core.Security /// /// /// + /// + /// Currently we do not suport a timed lock out, when they are locked out, an admin will have to reset the status + /// public Task SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset lockoutEnd) { if (user == null) throw new ArgumentNullException("user"); diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 51864e4323..eabc2bd248 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -563,23 +563,36 @@ namespace Umbraco.Web.Editors /// Unlocks the users with the given user ids /// /// - public HttpResponseMessage PostUnlockUsers([FromUri]int[] userIds) + public async Task PostUnlockUsers([FromUri]int[] userIds) { - var users = Services.UserService.GetUsersById(userIds).ToArray(); - foreach (var u in users) - { - u.IsLockedOut = false; - } - Services.UserService.Save(users); + if (userIds.Length <= 0) + return Request.CreateResponse(HttpStatusCode.OK); - if (users.Length > 1) + if (userIds.Length == 1) { + var unlockResult = await UserManager.SetLockoutEndDateAsync(userIds[0], DateTimeOffset.Now); + if (unlockResult.Succeeded == false) + { + return Request.CreateValidationErrorResponse( + string.Format("Could not unlock for user {0} - error {1}", userIds[0], unlockResult.Errors.First())); + } + var user = await UserManager.FindByIdAsync(userIds[0]); return Request.CreateNotificationSuccessResponse( - Services.TextService.Localize("speechBubbles/unlockUsersSuccess", new[] { userIds.Length.ToString() })); + Services.TextService.Localize("speechBubbles/unlockUserSuccess", new[] { user.Name })); + } + + foreach (var u in userIds) + { + var unlockResult = await UserManager.SetLockoutEndDateAsync(u, DateTimeOffset.Now); + if (unlockResult.Succeeded == false) + { + return Request.CreateValidationErrorResponse( + string.Format("Could not unlock for user {0} - error {1}", u, unlockResult.Errors.First())); + } } return Request.CreateNotificationSuccessResponse( - Services.TextService.Localize("speechBubbles/unlockUserSuccess", new[] { users[0].Name })); + Services.TextService.Localize("speechBubbles/unlockUsersSuccess", new[] { userIds.Length.ToString() })); } public HttpResponseMessage PostSetUserGroupsOnUsers([FromUri]string[] userGroupAliases, [FromUri]int[] userIds)