diff --git a/src/Umbraco.Core/Persistence/Mappers/UserMapper.cs b/src/Umbraco.Core/Persistence/Mappers/UserMapper.cs index 06e906143a..9d30f571b4 100644 --- a/src/Umbraco.Core/Persistence/Mappers/UserMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/UserMapper.cs @@ -43,6 +43,7 @@ namespace Umbraco.Core.Persistence.Mappers CacheMap(src => src.LastLockoutDate, dto => dto.LastLockoutDate); CacheMap(src => src.LastLoginDate, dto => dto.LastLoginDate); CacheMap(src => src.LastPasswordChangeDate, dto => dto.LastPasswordChangeDate); + CacheMap(src => src.SecurityStamp, dto => dto.SecurityStampToken); } #endregion diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index 6b798862b0..8aaa35f92e 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -9,7 +9,14 @@ namespace Umbraco.Core.Services /// Defines the UserService, which is an easy access to operations involving and eventually Users. /// public interface IUserService : IMembershipUserService - { + { + /// + /// Checks if a valid token is specified for an invited user and if so returns the user object + /// + /// + /// + IUser ValidateInviteToken(string token); + IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, UserState? userState = null, string[] userGroups = null, string filter = ""); diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index dd81824f6a..e542195a01 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -489,6 +489,30 @@ namespace Umbraco.Core.Services } } + public IUser ValidateInviteToken(string token) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateUserRepository(uow); + var query = new Query(); + + query.Where(member => member.SecurityStamp == token); + + var found = repository.GetByQuery(query).ToArray(); + + if (found.Length == 0) return null; + + var user = found[0]; + + //they must not be approved for an invite to work + if (user.IsApproved) return null; + //they should have never logged in for an invite to work + if (user.LastLoginDate != default(DateTime)) return null; + + return user; + } + } + public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, UserState? userState = null, string[] userGroups = null, string filter = "") { using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js index 40f1dbb807..d1e9c59b80 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js @@ -14,7 +14,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { return { get2FAProviders: function () { - + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( @@ -69,7 +69,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { * */ performLogin: function (username, password) { - + if (!username || !password) { return angularHelper.rejectedPromise({ errorMsg: 'Username or password cannot be empty' @@ -81,12 +81,30 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { umbRequestHelper.getApiUrl( "authenticationApiBaseUrl", "PostLogin"), { - username: username, - password: password - }), + username: username, + password: password + }), 'Login failed for user ' + username); }, + verifyInvite: function (token) { + + if (!token) { + return angularHelper.rejectedPromise({ + errorMsg: 'Token cannot be empty' + }); + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "PostVerifyInvite", { + token: token + })), + 'Failed to verify token ' + token); + }, + /** * @ngdoc method * @name umbraco.resources.authResource#performRequestPasswordReset @@ -114,7 +132,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { errorMsg: 'Email address cannot be empty' }); } - + //TODO: This validation shouldn't really be done here, the validation on the login dialog // is pretty hacky which is why this is here, ideally validation on the login dialog would // be done properly. @@ -130,11 +148,11 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { umbRequestHelper.getApiUrl( "authenticationApiBaseUrl", "PostRequestPasswordReset"), { - email: email - }), + email: email + }), 'Request password reset failed for email ' + email); }, - + /** * @ngdoc method * @name umbraco.resources.authResource#performValidatePasswordResetCode @@ -173,11 +191,11 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { $http.post( umbRequestHelper.getApiUrl( "authenticationApiBaseUrl", - "PostValidatePasswordResetCode"), - { - userId: userId, - resetCode: resetCode - }), + "PostValidatePasswordResetCode"), + { + userId: userId, + resetCode: resetCode + }), 'Password reset code validation failed for userId ' + userId + ', code' + resetCode); }, @@ -234,11 +252,11 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { umbRequestHelper.getApiUrl( "authenticationApiBaseUrl", "PostSetPassword"), - { - userId: userId, - password: password, - resetCode: resetCode - }), + { + userId: userId, + password: password, + resetCode: resetCode + }), 'Password reset code validation failed for userId ' + userId); }, @@ -254,9 +272,9 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { umbRequestHelper.getApiUrl( "authenticationApiBaseUrl", "PostUnLinkLogin"), { - loginProvider: loginProvider, - providerKey: providerKey - }), + loginProvider: loginProvider, + providerKey: providerKey + }), 'Unlinking login provider failed'); }, @@ -278,14 +296,14 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { * @returns {Promise} resourcePromise object * */ - performLogout: function() { + performLogout: function () { return umbRequestHelper.resourcePromise( $http.post( umbRequestHelper.getApiUrl( "authenticationApiBaseUrl", "PostLogout"))); }, - + /** * @ngdoc method * @name umbraco.resources.authResource#getCurrentUser @@ -305,13 +323,13 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { * */ getCurrentUser: function () { - + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "authenticationApiBaseUrl", "GetCurrentUser")), - 'Server call failed for getting current user'); + 'Server call failed for getting current user'); }, getCurrentUserLinkedLogins: function () { @@ -323,7 +341,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { "GetCurrentUserLinkedLogins")), 'Server call failed for getting current users linked logins'); }, - + /** * @ngdoc method * @name umbraco.resources.authResource#isAuthenticated @@ -357,7 +375,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { } return data; }, - error: function (data, status, headers, config) { + error: function (data, status, headers, config) { return { errorMsg: 'Server call failed for checking authentication', data: data, diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js index 106e748c20..c925598bcd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js @@ -1,23 +1,29 @@ angular.module("umbraco").controller("Umbraco.Dialogs.LoginController", function ($scope, $cookies, $location, localizationService, userService, externalLoginInfo, resetPasswordCodeInfo, $timeout, authResource, dialogService) { - $scope.isInvite = false; + $scope.invitedUser = null; function init() { // Check if it is a new user if ($location.search().invite) { - $scope.isInvite = true; - $scope.inviteSetPassword = true; + var token = $location.search().invite; + authResource.verifyInvite(token).then(function (data) { + $scope.invitedUser = data; + $scope.inviteSetPassword = true; + }, function () { + //it failed so we should remove the search + $location.search('invite', null); + }); } } - $scope.inviteSavePassword = function() { + $scope.inviteSavePassword = function () { $scope.inviteSetPassword = false; $scope.inviteSetAvatar = true; }; - var setFieldFocus = function(form, field) { - $timeout(function() { + var setFieldFocus = function (form, field) { + $timeout(function () { $("form[name='" + form + "'] input[name='" + field + "']").focus(); }); } @@ -112,7 +118,7 @@ } $scope.loginSubmit = function (login, password) { - + //if the login and password are not empty we need to automatically // validate them - this is because if there are validation errors on the server // then the user has to change both username & password to resubmit which isn't ideal, @@ -127,24 +133,24 @@ } userService.authenticate(login, password) - .then(function(data) { - $scope.submit(true); - }, - function(reason) { + .then(function (data) { + $scope.submit(true); + }, + function (reason) { - //is Two Factor required? - if (reason.status === 402) { - $scope.errorMsg = "Additional authentication required"; - show2FALoginDialog(reason.data.twoFactorView, $scope.submit); - } - else { - $scope.errorMsg = reason.errorMsg; + //is Two Factor required? + if (reason.status === 402) { + $scope.errorMsg = "Additional authentication required"; + show2FALoginDialog(reason.data.twoFactorView, $scope.submit); + } + else { + $scope.errorMsg = reason.errorMsg; - //set the form inputs to invalid - $scope.loginForm.username.$setValidity("auth", false); - $scope.loginForm.password.$setValidity("auth", false); - } - }); + //set the form inputs to invalid + $scope.loginForm.username.$setValidity("auth", false); + $scope.loginForm.password.$setValidity("auth", false); + } + }); //setup a watch for both of the model values changing, if they change // while the form is invalid, then revalidate them so that the form can @@ -166,7 +172,7 @@ if (email && email.length > 0) { $scope.requestPasswordResetForm.email.$setValidity('auth', true); } - + $scope.showEmailResetConfirmation = false; if ($scope.requestPasswordResetForm.$invalid) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index 991b52cc62..49b4cadc0d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -8,10 +8,10 @@ -