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.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index 0acd30afe0..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,63 @@ '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"; + } + + //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(",")); + } + + /** + * @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="); @@ -67,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, @@ -119,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( @@ -130,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"; @@ -147,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"; @@ -164,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"; @@ -185,6 +410,7 @@ var resource = { disableUsers: disableUsers, enableUsers: enableUsers, + unlockUsers: unlockUsers, setUserGroupsOnUsers: setUserGroupsOnUsers, getPagedResults: getPagedResults, getUser: getUser, 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 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 e060eaf393..a9b17998d5 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; @@ -254,11 +255,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); }); @@ -267,16 +268,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 94c8360346..8dd5a74e5d 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 @@
- + +
@@ -234,29 +236,43 @@
-
- - -
+ size="s"> + +
+ +
+ + +
+ +
+
@@ -268,7 +284,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 69db1a1da1..4df493cfe6 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 }); } @@ -560,6 +584,7 @@ // reset all states vm.allowDisableUser = true; vm.allowEnableUser = true; + vm.allowUnlockUser = true; vm.allowSetUserGroup = true; var firstSelectedUserGroups; @@ -574,6 +599,7 @@ if (user.isCurrentUser) { vm.allowDisableUser = false; vm.allowEnableUser = false; + vm.allowUnlockUser = false; vm.allowSetUserGroup = false; return; } @@ -590,6 +616,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 @@ -1216,6 +1217,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..eabc2bd248 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -557,6 +557,42 @@ 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 async Task PostUnlockUsers([FromUri]int[] userIds) + { + if (userIds.Length <= 0) + return Request.CreateResponse(HttpStatusCode.OK); + + 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/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/unlockUsersSuccess", new[] { userIds.Length.ToString() })); } public HttpResponseMessage PostSetUserGroupsOnUsers([FromUri]string[] userGroupAliases, [FromUri]int[] userIds)