From f09f17e4967fd2670119a1fd5354a40d3e966745 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 13 Jun 2017 18:38:16 +0200 Subject: [PATCH] getting email invite working and with identity apis --- src/Umbraco.Core/EnumerableExtensions.cs | 3 + .../Models/Identity/BackOfficeIdentityUser.cs | 2 + src/Umbraco.Core/Models/Membership/IUser.cs | 5 +- src/Umbraco.Core/Models/Membership/User.cs | 9 +- src/Umbraco.Core/Models/Rdbms/UserDto.cs | 4 + .../Persistence/Factories/UserFactory.cs | 3 +- .../UpdateUserTables.cs | 3 + .../Repositories/UserRepository.cs | 3 +- .../Security/BackOfficeUserStore.cs | 13 +- src/Umbraco.Core/Services/IUserService.cs | 9 +- src/Umbraco.Core/Services/UserService.cs | 24 +- .../src/common/resources/auth.resource.js | 789 +++++++++--------- .../views/common/dialogs/login.controller.js | 490 +++++------ .../src/views/common/dialogs/login.html | 391 +++++---- .../Editors/AuthenticationController.cs | 23 +- src/Umbraco.Web/Editors/UsersController.cs | 58 +- .../Models/Mapping/UserModelMapper.cs | 47 +- 17 files changed, 952 insertions(+), 924 deletions(-) diff --git a/src/Umbraco.Core/EnumerableExtensions.cs b/src/Umbraco.Core/EnumerableExtensions.cs index e8565f3bc7..79a703ca5f 100644 --- a/src/Umbraco.Core/EnumerableExtensions.cs +++ b/src/Umbraco.Core/EnumerableExtensions.cs @@ -111,6 +111,9 @@ namespace Umbraco.Core /// public static bool ContainsAll(this IEnumerable source, IEnumerable other) { + if (source == null) throw new ArgumentNullException("source"); + if (other == null) throw new ArgumentNullException("other"); + return other.Except(source).Any() == false; } diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index 52f56fc2d5..8657bc4c6c 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -16,6 +16,8 @@ namespace Umbraco.Core.Models.Identity { StartMediaIds = new int[] { }; StartContentIds = new int[] { }; + Groups = new string[] { }; + AllowedSections = new string[] { }; Culture = Configuration.GlobalSettings.DefaultUILanguage; } diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index d6af54f937..3e45166d40 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Models.Membership @@ -17,6 +18,8 @@ namespace Umbraco.Core.Models.Membership int[] StartMediaIds { get; set; } string Language { get; set; } + DateTime? EmailConfirmedDate { get; set; } + /// /// Gets the groups that user is part of /// diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index b2e380811b..70fb643bfb 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -102,6 +102,7 @@ namespace Umbraco.Core.Models.Membership private int _failedLoginAttempts; private string _username; + private DateTime? _emailConfirmedDate; private string _email; private string _rawPasswordValue; private IEnumerable _allowedSections; @@ -137,6 +138,7 @@ namespace Umbraco.Core.Models.Membership public readonly PropertyInfo IsLockedOutSelector = ExpressionHelper.GetPropertyInfo(x => x.IsLockedOut); public readonly PropertyInfo IsApprovedSelector = ExpressionHelper.GetPropertyInfo(x => x.IsApproved); public readonly PropertyInfo LanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.Language); + public readonly PropertyInfo EmailConfirmedDateSelector = ExpressionHelper.GetPropertyInfo(x => x.EmailConfirmedDate); public readonly PropertyInfo DefaultToLiveEditingSelector = ExpressionHelper.GetPropertyInfo(x => x.DefaultToLiveEditing); @@ -158,7 +160,12 @@ namespace Umbraco.Core.Models.Membership set { throw new NotSupportedException("Cannot set the provider user key for a user"); } } - + [DataMember] + public DateTime? EmailConfirmedDate + { + get { return _emailConfirmedDate; } + set { SetPropertyValueAndDetectChanges(value, ref _emailConfirmedDate, Ps.Value.EmailConfirmedDateSelector); } + } [DataMember] public string Username { diff --git a/src/Umbraco.Core/Models/Rdbms/UserDto.cs b/src/Umbraco.Core/Models/Rdbms/UserDto.cs index e804a949da..a55eece583 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserDto.cs @@ -70,6 +70,10 @@ namespace Umbraco.Core.Models.Rdbms [NullSetting(NullSetting = NullSettings.Null)] public DateTime? LastLoginDate { get; set; } + [Column("emailConfirmedDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? EmailConfirmedDate { get; set; } + [Column("createDate")] [NullSetting(NullSetting = NullSettings.NotNull)] [Constraint(Default = SystemMethods.CurrentDateTime)] diff --git a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs index cae42191c6..40a7dbe579 100644 --- a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs @@ -65,7 +65,8 @@ namespace Umbraco.Core.Persistence.Factories LastPasswordChangeDate = entity.LastPasswordChangeDate == DateTime.MinValue ? (DateTime?)null : entity.LastPasswordChangeDate, CreateDate = entity.CreateDate, UpdateDate = entity.UpdateDate, - Avatar = entity.Avatar + Avatar = entity.Avatar, + EmailConfirmedDate = entity.EmailConfirmedDate }; foreach (var startNodeId in entity.StartContentIds) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/UpdateUserTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/UpdateUserTables.cs index 04629e74c3..b91080bf2e 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/UpdateUserTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/UpdateUserTables.cs @@ -22,6 +22,9 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZe if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("updateDate")) == false) Create.Column("updateDate").OnTable("umbracoUser").AsDateTime().NotNullable().WithDefault(SystemMethods.CurrentDateTime); + + if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("emailConfirmedDate")) == false) + Create.Column("emailConfirmedDate").OnTable("umbracoUser").AsDateTime().Nullable(); } public override void Down() diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index 96c3575596..9f2b915265 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -270,7 +270,8 @@ namespace Umbraco.Core.Persistence.Repositories {"failedLoginAttempts", "FailedPasswordAttempts"}, {"createDate", "CreateDate"}, {"updateDate", "UpdateDate"}, - {"avatar", "Avatar"} + {"avatar", "Avatar"}, + {"emailConfirmedDate", "EmailConfirmedDate"} }; //create list of properties that have changed diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 3dcb74b62f..3d43285e4c 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -276,7 +276,9 @@ namespace Umbraco.Core.Security public Task GetEmailConfirmedAsync(BackOfficeIdentityUser user) { ThrowIfDisposed(); - throw new NotImplementedException(); + if (user == null) throw new ArgumentNullException("user"); + + return Task.FromResult(user.EmailConfirmed); } /// @@ -287,7 +289,8 @@ namespace Umbraco.Core.Security public Task SetEmailConfirmedAsync(BackOfficeIdentityUser user, bool confirmed) { ThrowIfDisposed(); - throw new NotImplementedException(); + user.EmailConfirmed = confirmed; + return Task.FromResult(0); } /// @@ -630,6 +633,12 @@ namespace Umbraco.Core.Security anythingChanged = true; user.LastLoginDate = identityUser.LastLoginDateUtc.Value.ToLocalTime(); } + if ((user.EmailConfirmedDate.HasValue && user.EmailConfirmedDate.Value != default(DateTime) && identityUser.EmailConfirmed == false) + || ((user.EmailConfirmedDate.HasValue == false || user.EmailConfirmedDate.Value == default(DateTime)) && identityUser.EmailConfirmed)) + { + anythingChanged = true; + user.EmailConfirmedDate = identityUser.EmailConfirmed ? (DateTime?)DateTime.Now : null; + } if (user.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false) { anythingChanged = true; diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index 8aaa35f92e..6b798862b0 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -9,14 +9,7 @@ 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 abde2b1750..f589916c3b 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -487,29 +487,7 @@ namespace Umbraco.Core.Services return ret; } - } - - 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 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 = "") { 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 d1e9c59b80..fbd6b2b3ed 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 @@ -11,409 +11,416 @@ */ function authResource($q, $http, umbRequestHelper, angularHelper) { - return { + return { - get2FAProviders: function () { + get2FAProviders: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "Get2FAProviders")), - 'Could not retrive two factor provider info'); - }, + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "Get2FAProviders")), + 'Could not retrive two factor provider info'); + }, - send2FACode: function (provider) { + send2FACode: function (provider) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "PostSend2FACode"), - angular.toJson(provider)), - 'Could not send code'); - }, + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "PostSend2FACode"), + angular.toJson(provider)), + 'Could not send code'); + }, - verify2FACode: function (provider, code) { + verify2FACode: function (provider, code) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "PostVerify2FACode"), - { - code: code, - provider: provider - }), - 'Could not verify code'); - }, + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "PostVerify2FACode"), + { + code: code, + provider: provider + }), + 'Could not verify code'); + }, - /** - * @ngdoc method - * @name umbraco.resources.authResource#performLogin - * @methodOf umbraco.resources.authResource - * - * @description - * Logs the Umbraco backoffice user in if the credentials are good - * - * ##usage - *
-         * authResource.performLogin(login, password)
-         *    .then(function(data) {
-         *        //Do stuff for login...
-         *    });
-         * 
- * @param {string} login Username of backoffice user - * @param {string} password Password of backoffice user - * @returns {Promise} resourcePromise object - * - */ - performLogin: function (username, password) { + /** + * @ngdoc method + * @name umbraco.resources.authResource#performLogin + * @methodOf umbraco.resources.authResource + * + * @description + * Logs the Umbraco backoffice user in if the credentials are good + * + * ##usage + *
+     * authResource.performLogin(login, password)
+     *    .then(function(data) {
+     *        //Do stuff for login...
+     *    });
+     * 
+ * @param {string} login Username of backoffice user + * @param {string} password Password of backoffice user + * @returns {Promise} resourcePromise object + * + */ + performLogin: function (username, password) { - if (!username || !password) { - return angularHelper.rejectedPromise({ - errorMsg: 'Username or password cannot be empty' - }); + if (!username || !password) { + return angularHelper.rejectedPromise({ + errorMsg: 'Username or password cannot be empty' + }); + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "PostLogin"), { + username: username, + password: password + }), + 'Login failed for user ' + username); + }, + + verifyInvite: function (userId, token) { + + if (!token) { + return angularHelper.rejectedPromise({ + errorMsg: 'token cannot be empty' + }); + } + + if (!userId) { + return angularHelper.rejectedPromise({ + errorMsg: 'userId cannot be empty' + }); + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "PostVerifyInvite", { + id: userId, + token: token + })), + 'Failed to verify token ' + token); + }, + + /** + * @ngdoc method + * @name umbraco.resources.authResource#performRequestPasswordReset + * @methodOf umbraco.resources.authResource + * + * @description + * Checks to see if the provided email address is a valid user account and sends a link + * to allow them to reset their password + * + * ##usage + *
+     * authResource.performRequestPasswordReset(email)
+     *    .then(function(data) {
+     *        //Do stuff for password reset request...
+     *    });
+     * 
+ * @param {string} email Email address of backoffice user + * @returns {Promise} resourcePromise object + * + */ + performRequestPasswordReset: function (email) { + + if (!email) { + return angularHelper.rejectedPromise({ + 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. + var emailRegex = /\S+@\S+\.\S+/; + if (!emailRegex.test(email)) { + return angularHelper.rejectedPromise({ + errorMsg: 'Email address is not valid' + }); + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "PostRequestPasswordReset"), { + email: email + }), + 'Request password reset failed for email ' + email); + }, + + /** + * @ngdoc method + * @name umbraco.resources.authResource#performValidatePasswordResetCode + * @methodOf umbraco.resources.authResource + * + * @description + * Checks to see if the provided password reset code is valid + * + * ##usage + *
+     * authResource.performValidatePasswordResetCode(resetCode)
+     *    .then(function(data) {
+     *        //Allow reset of password
+     *    });
+     * 
+ * @param {integer} userId User Id + * @param {string} resetCode Password reset code + * @returns {Promise} resourcePromise object + * + */ + performValidatePasswordResetCode: function (userId, resetCode) { + + if (!userId) { + return angularHelper.rejectedPromise({ + errorMsg: 'User Id cannot be empty' + }); + } + + if (!resetCode) { + return angularHelper.rejectedPromise({ + errorMsg: 'Reset code cannot be empty' + }); + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "PostValidatePasswordResetCode"), + { + userId: userId, + resetCode: resetCode + }), + 'Password reset code validation failed for userId ' + userId + ', code' + resetCode); + }, + + /** + * @ngdoc method + * @name umbraco.resources.authResource#performSetPassword + * @methodOf umbraco.resources.authResource + * + * @description + * Checks to see if the provided password reset code is valid and sets the user's password + * + * ##usage + *
+     * authResource.performSetPassword(userId, password, confirmPassword, resetCode)
+     *    .then(function(data) {
+     *        //Password set
+     *    });
+     * 
+ * @param {integer} userId User Id + * @param {string} password New password + * @param {string} confirmPassword Confirmation of new password + * @param {string} resetCode Password reset code + * @returns {Promise} resourcePromise object + * + */ + performSetPassword: function (userId, password, confirmPassword, resetCode) { + + if (userId === undefined || userId === null) { + return angularHelper.rejectedPromise({ + errorMsg: 'User Id cannot be empty' + }); + } + + if (!password) { + return angularHelper.rejectedPromise({ + errorMsg: 'Password cannot be empty' + }); + } + + if (password !== confirmPassword) { + return angularHelper.rejectedPromise({ + errorMsg: 'Password and confirmation do not match' + }); + } + + if (!resetCode) { + return angularHelper.rejectedPromise({ + errorMsg: 'Reset code cannot be empty' + }); + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "PostSetPassword"), + { + userId: userId, + password: password, + resetCode: resetCode + }), + 'Password reset code validation failed for userId ' + userId); + }, + + unlinkLogin: function (loginProvider, providerKey) { + if (!loginProvider || !providerKey) { + return angularHelper.rejectedPromise({ + errorMsg: 'loginProvider or providerKey cannot be empty' + }); + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "PostUnLinkLogin"), { + loginProvider: loginProvider, + providerKey: providerKey + }), + 'Unlinking login provider failed'); + }, + + /** + * @ngdoc method + * @name umbraco.resources.authResource#performLogout + * @methodOf umbraco.resources.authResource + * + * @description + * Logs out the Umbraco backoffice user + * + * ##usage + *
+     * authResource.performLogout()
+     *    .then(function(data) {
+     *        //Do stuff for logging out...
+     *    });
+     * 
+ * @returns {Promise} resourcePromise object + * + */ + performLogout: function () { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "PostLogout"))); + }, + + /** + * @ngdoc method + * @name umbraco.resources.authResource#getCurrentUser + * @methodOf umbraco.resources.authResource + * + * @description + * Sends a request to the server to get the current user details, will return a 401 if the user is not logged in + * + * ##usage + *
+     * authResource.getCurrentUser()
+     *    .then(function(data) {
+     *        //Do stuff for fetching the current logged in Umbraco backoffice user
+     *    });
+     * 
+ * @returns {Promise} resourcePromise object + * + */ + getCurrentUser: function () { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "GetCurrentUser")), + 'Server call failed for getting current user'); + }, + + getCurrentUserLinkedLogins: function () { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "GetCurrentUserLinkedLogins")), + 'Server call failed for getting current users linked logins'); + }, + + /** + * @ngdoc method + * @name umbraco.resources.authResource#isAuthenticated + * @methodOf umbraco.resources.authResource + * + * @description + * Checks if the user is logged in or not - does not return 401 or 403 + * + * ##usage + *
+     * authResource.isAuthenticated()
+     *    .then(function(data) {
+     *        //Do stuff to check if user is authenticated
+     *    });
+     * 
+ * @returns {Promise} resourcePromise object + * + */ + isAuthenticated: function () { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "IsAuthenticated")), + { + success: function (data, status, headers, config) { + //if the response is false, they are not logged in so return a rejection + if (data === false || data === "false") { + return $q.reject('User is not logged in'); } + return data; + }, + error: function (data, status, headers, config) { + return { + errorMsg: 'Server call failed for checking authentication', + data: data, + status: status + }; + } + }); + }, - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "PostLogin"), { - username: username, - password: password - }), - 'Login failed for user ' + username); - }, + /** + * @ngdoc method + * @name umbraco.resources.authResource#getRemainingTimeoutSeconds + * @methodOf umbraco.resources.authResource + * + * @description + * Gets the user's remaining seconds before their login times out + * + * ##usage + *
+     * authResource.getRemainingTimeoutSeconds()
+     *    .then(function(data) {
+     *        //Number of seconds is returned
+     *    });
+     * 
+ * @returns {Promise} resourcePromise object + * + */ + getRemainingTimeoutSeconds: function () { - verifyInvite: function (token) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "GetRemainingTimeoutSeconds")), + 'Server call failed for checking remaining seconds'); + } - 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 - * @methodOf umbraco.resources.authResource - * - * @description - * Checks to see if the provided email address is a valid user account and sends a link - * to allow them to reset their password - * - * ##usage - *
-         * authResource.performRequestPasswordReset(email)
-         *    .then(function(data) {
-         *        //Do stuff for password reset request...
-         *    });
-         * 
- * @param {string} email Email address of backoffice user - * @returns {Promise} resourcePromise object - * - */ - performRequestPasswordReset: function (email) { - - if (!email) { - return angularHelper.rejectedPromise({ - 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. - var emailRegex = /\S+@\S+\.\S+/; - if (!emailRegex.test(email)) { - return angularHelper.rejectedPromise({ - errorMsg: 'Email address is not valid' - }); - } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "PostRequestPasswordReset"), { - email: email - }), - 'Request password reset failed for email ' + email); - }, - - /** - * @ngdoc method - * @name umbraco.resources.authResource#performValidatePasswordResetCode - * @methodOf umbraco.resources.authResource - * - * @description - * Checks to see if the provided password reset code is valid - * - * ##usage - *
-         * authResource.performValidatePasswordResetCode(resetCode)
-         *    .then(function(data) {
-         *        //Allow reset of password
-         *    });
-         * 
- * @param {integer} userId User Id - * @param {string} resetCode Password reset code - * @returns {Promise} resourcePromise object - * - */ - performValidatePasswordResetCode: function (userId, resetCode) { - - if (!userId) { - return angularHelper.rejectedPromise({ - errorMsg: 'User Id cannot be empty' - }); - } - - if (!resetCode) { - return angularHelper.rejectedPromise({ - errorMsg: 'Reset code cannot be empty' - }); - } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "PostValidatePasswordResetCode"), - { - userId: userId, - resetCode: resetCode - }), - 'Password reset code validation failed for userId ' + userId + ', code' + resetCode); - }, - - /** - * @ngdoc method - * @name umbraco.resources.authResource#performSetPassword - * @methodOf umbraco.resources.authResource - * - * @description - * Checks to see if the provided password reset code is valid and sets the user's password - * - * ##usage - *
-         * authResource.performSetPassword(userId, password, confirmPassword, resetCode)
-         *    .then(function(data) {
-         *        //Password set
-         *    });
-         * 
- * @param {integer} userId User Id - * @param {string} password New password - * @param {string} confirmPassword Confirmation of new password - * @param {string} resetCode Password reset code - * @returns {Promise} resourcePromise object - * - */ - performSetPassword: function (userId, password, confirmPassword, resetCode) { - - if (userId === undefined || userId === null) { - return angularHelper.rejectedPromise({ - errorMsg: 'User Id cannot be empty' - }); - } - - if (!password) { - return angularHelper.rejectedPromise({ - errorMsg: 'Password cannot be empty' - }); - } - - if (password !== confirmPassword) { - return angularHelper.rejectedPromise({ - errorMsg: 'Password and confirmation do not match' - }); - } - - if (!resetCode) { - return angularHelper.rejectedPromise({ - errorMsg: 'Reset code cannot be empty' - }); - } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "PostSetPassword"), - { - userId: userId, - password: password, - resetCode: resetCode - }), - 'Password reset code validation failed for userId ' + userId); - }, - - unlinkLogin: function (loginProvider, providerKey) { - if (!loginProvider || !providerKey) { - return angularHelper.rejectedPromise({ - errorMsg: 'loginProvider or providerKey cannot be empty' - }); - } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "PostUnLinkLogin"), { - loginProvider: loginProvider, - providerKey: providerKey - }), - 'Unlinking login provider failed'); - }, - - /** - * @ngdoc method - * @name umbraco.resources.authResource#performLogout - * @methodOf umbraco.resources.authResource - * - * @description - * Logs out the Umbraco backoffice user - * - * ##usage - *
-         * authResource.performLogout()
-         *    .then(function(data) {
-         *        //Do stuff for logging out...
-         *    });
-         * 
- * @returns {Promise} resourcePromise object - * - */ - performLogout: function () { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "PostLogout"))); - }, - - /** - * @ngdoc method - * @name umbraco.resources.authResource#getCurrentUser - * @methodOf umbraco.resources.authResource - * - * @description - * Sends a request to the server to get the current user details, will return a 401 if the user is not logged in - * - * ##usage - *
-         * authResource.getCurrentUser()
-         *    .then(function(data) {
-         *        //Do stuff for fetching the current logged in Umbraco backoffice user
-         *    });
-         * 
- * @returns {Promise} resourcePromise object - * - */ - getCurrentUser: function () { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "GetCurrentUser")), - 'Server call failed for getting current user'); - }, - - getCurrentUserLinkedLogins: function () { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "GetCurrentUserLinkedLogins")), - 'Server call failed for getting current users linked logins'); - }, - - /** - * @ngdoc method - * @name umbraco.resources.authResource#isAuthenticated - * @methodOf umbraco.resources.authResource - * - * @description - * Checks if the user is logged in or not - does not return 401 or 403 - * - * ##usage - *
-         * authResource.isAuthenticated()
-         *    .then(function(data) {
-         *        //Do stuff to check if user is authenticated
-         *    });
-         * 
- * @returns {Promise} resourcePromise object - * - */ - isAuthenticated: function () { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "IsAuthenticated")), - { - success: function (data, status, headers, config) { - //if the response is false, they are not logged in so return a rejection - if (data === false || data === "false") { - return $q.reject('User is not logged in'); - } - return data; - }, - error: function (data, status, headers, config) { - return { - errorMsg: 'Server call failed for checking authentication', - data: data, - status: status - }; - } - }); - }, - - /** - * @ngdoc method - * @name umbraco.resources.authResource#getRemainingTimeoutSeconds - * @methodOf umbraco.resources.authResource - * - * @description - * Gets the user's remaining seconds before their login times out - * - * ##usage - *
-         * authResource.getRemainingTimeoutSeconds()
-         *    .then(function(data) {
-         *        //Number of seconds is returned
-         *    });
-         * 
- * @returns {Promise} resourcePromise object - * - */ - getRemainingTimeoutSeconds: function () { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "authenticationApiBaseUrl", - "GetRemainingTimeoutSeconds")), - 'Server call failed for checking remaining seconds'); - } - - }; + }; } angular.module('umbraco.resources').factory('authResource', authResource); 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 c925598bcd..7cc72bcfa9 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,260 +1,264 @@ angular.module("umbraco").controller("Umbraco.Dialogs.LoginController", - function ($scope, $cookies, $location, localizationService, userService, externalLoginInfo, resetPasswordCodeInfo, $timeout, authResource, dialogService) { + function ($scope, $cookies, $location, localizationService, userService, externalLoginInfo, resetPasswordCodeInfo, $timeout, authResource, dialogService) { - $scope.invitedUser = null; + $scope.invitedUser = null; - function init() { - // Check if it is a new user - if ($location.search().invite) { - 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); - }); - } + function init() { + // Check if it is a new user + if ($location.search().invite) { + var token = decodeURIComponent($location.search().invite); + //it's split by pipe so split it + var parts = token.split("|"); + authResource.verifyInvite(parts[0], parts[1]).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.inviteSetPassword = false; + $scope.inviteSetAvatar = true; + }; + + var setFieldFocus = function (form, field) { + $timeout(function () { + $("form[name='" + form + "'] input[name='" + field + "']").focus(); + }); + } + + var twoFactorloginDialog = null; + function show2FALoginDialog(view, callback) { + if (!twoFactorloginDialog) { + twoFactorloginDialog = dialogService.open({ + + //very special flag which means that global events cannot close this dialog + manualClose: true, + template: view, + modalClass: "login-overlay", + animation: "slide", + show: true, + callback: callback, + + }); + } + } + + function resetInputValidation() { + $scope.confirmPassword = ""; + $scope.password = ""; + $scope.login = ""; + if ($scope.loginForm) { + $scope.loginForm.username.$setValidity('auth', true); + $scope.loginForm.password.$setValidity('auth', true); + } + if ($scope.requestPasswordResetForm) { + $scope.requestPasswordResetForm.email.$setValidity("auth", true); + } + if ($scope.setPasswordForm) { + $scope.setPasswordForm.password.$setValidity('auth', true); + $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); + } + } + + $scope.allowPasswordReset = Umbraco.Sys.ServerVariables.umbracoSettings.allowPasswordReset; + + $scope.showLogin = function () { + $scope.errorMsg = ""; + resetInputValidation(); + $scope.view = "login"; + setFieldFocus("loginForm", "username"); + } + + $scope.showRequestPasswordReset = function () { + $scope.errorMsg = ""; + resetInputValidation(); + $scope.view = "request-password-reset"; + $scope.showEmailResetConfirmation = false; + setFieldFocus("requestPasswordResetForm", "email"); + } + + $scope.showSetPassword = function () { + $scope.errorMsg = ""; + resetInputValidation(); + $scope.view = "set-password"; + setFieldFocus("setPasswordForm", "password"); + } + + var d = new Date(); + var konamiGreetings = new Array("Suze Sunday", "Malibu Monday", "Tequila Tuesday", "Whiskey Wednesday", "Negroni Day", "Fernet Friday", "Sancerre Saturday"); + var konamiMode = $cookies.konamiLogin; + if (konamiMode == "1") { + $scope.greeting = "Happy " + konamiGreetings[d.getDay()]; + } else { + localizationService.localize("login_greeting" + d.getDay()).then(function (label) { + $scope.greeting = label; + }); // weekday[d.getDay()]; + } + $scope.errorMsg = ""; + + $scope.externalLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl; + $scope.externalLoginProviders = externalLoginInfo.providers; + $scope.externalLoginInfo = externalLoginInfo; + $scope.resetPasswordCodeInfo = resetPasswordCodeInfo; + $scope.backgroundImage = Umbraco.Sys.ServerVariables.umbracoSettings.loginBackgroundImage; + + $scope.activateKonamiMode = function () { + if ($cookies.konamiLogin == "1") { + // somehow I can't update the cookie value using $cookies, so going native + document.cookie = "konamiLogin=; expires=Thu, 01 Jan 1970 00:00:01 GMT;"; + document.location.reload(); + } else { + document.cookie = "konamiLogin=1; expires=Tue, 01 Jan 2030 00:00:01 GMT;"; + $scope.$apply(function () { + $scope.greeting = "Happy " + konamiGreetings[d.getDay()]; + }); + } + } + + $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, + // so if they're not empty, we'll just make sure to set them to valid. + if (login && password && login.length > 0 && password.length > 0) { + $scope.loginForm.username.$setValidity('auth', true); + $scope.loginForm.password.$setValidity('auth', true); + } + + if ($scope.loginForm.$invalid) { + return; + } + + userService.authenticate(login, password) + .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; + + //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 + // be submitted again. + $scope.loginForm.username.$viewChangeListeners.push(function () { + if ($scope.loginForm.username.$invalid) { + $scope.loginForm.username.$setValidity('auth', true); } - - $scope.inviteSavePassword = function () { - $scope.inviteSetPassword = false; - $scope.inviteSetAvatar = true; - }; - - var setFieldFocus = function (form, field) { - $timeout(function () { - $("form[name='" + form + "'] input[name='" + field + "']").focus(); - }); + }); + $scope.loginForm.password.$viewChangeListeners.push(function () { + if ($scope.loginForm.password.$invalid) { + $scope.loginForm.password.$setValidity('auth', true); } + }); + }; - var twoFactorloginDialog = null; - function show2FALoginDialog(view, callback) { - if (!twoFactorloginDialog) { - twoFactorloginDialog = dialogService.open({ + $scope.requestPasswordResetSubmit = function (email) { - //very special flag which means that global events cannot close this dialog - manualClose: true, - template: view, - modalClass: "login-overlay", - animation: "slide", - show: true, - callback: callback, + if (email && email.length > 0) { + $scope.requestPasswordResetForm.email.$setValidity('auth', true); + } - }); - } + $scope.showEmailResetConfirmation = false; + + if ($scope.requestPasswordResetForm.$invalid) { + return; + } + + $scope.errorMsg = ""; + + authResource.performRequestPasswordReset(email) + .then(function () { + //remove the email entered + $scope.email = ""; + $scope.showEmailResetConfirmation = true; + }, function (reason) { + $scope.errorMsg = reason.errorMsg; + $scope.requestPasswordResetForm.email.$setValidity("auth", false); + }); + + $scope.requestPasswordResetForm.email.$viewChangeListeners.push(function () { + if ($scope.requestPasswordResetForm.email.$invalid) { + $scope.requestPasswordResetForm.email.$setValidity('auth', true); } + }); + }; - function resetInputValidation() { - $scope.confirmPassword = ""; - $scope.password = ""; - $scope.login = ""; - if ($scope.loginForm) { - $scope.loginForm.username.$setValidity('auth', true); - $scope.loginForm.password.$setValidity('auth', true); - } - if ($scope.requestPasswordResetForm) { - $scope.requestPasswordResetForm.email.$setValidity("auth", true); - } - if ($scope.setPasswordForm) { - $scope.setPasswordForm.password.$setValidity('auth', true); - $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); - } + $scope.setPasswordSubmit = function (password, confirmPassword) { + + $scope.showSetPasswordConfirmation = false; + + if (password && confirmPassword && password.length > 0 && confirmPassword.length > 0) { + $scope.setPasswordForm.password.$setValidity('auth', true); + $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); + } + + if ($scope.setPasswordForm.$invalid) { + return; + } + + authResource.performSetPassword($scope.resetPasswordCodeInfo.resetCodeModel.userId, password, confirmPassword, $scope.resetPasswordCodeInfo.resetCodeModel.resetCode) + .then(function () { + $scope.showSetPasswordConfirmation = true; + $scope.resetComplete = true; + + //reset the values in the resetPasswordCodeInfo angular so if someone logs out the change password isn't shown again + resetPasswordCodeInfo.resetCodeModel = null; + + }, function (reason) { + if (reason.data && reason.data.Message) { + $scope.errorMsg = reason.data.Message; + } + else { + $scope.errorMsg = reason.errorMsg; + } + $scope.setPasswordForm.password.$setValidity("auth", false); + $scope.setPasswordForm.confirmPassword.$setValidity("auth", false); + }); + + $scope.setPasswordForm.password.$viewChangeListeners.push(function () { + if ($scope.setPasswordForm.password.$invalid) { + $scope.setPasswordForm.password.$setValidity('auth', true); } - - $scope.allowPasswordReset = Umbraco.Sys.ServerVariables.umbracoSettings.allowPasswordReset; - - $scope.showLogin = function () { - $scope.errorMsg = ""; - resetInputValidation(); - $scope.view = "login"; - setFieldFocus("loginForm", "username"); - } - - $scope.showRequestPasswordReset = function () { - $scope.errorMsg = ""; - resetInputValidation(); - $scope.view = "request-password-reset"; - $scope.showEmailResetConfirmation = false; - setFieldFocus("requestPasswordResetForm", "email"); - } - - $scope.showSetPassword = function () { - $scope.errorMsg = ""; - resetInputValidation(); - $scope.view = "set-password"; - setFieldFocus("setPasswordForm", "password"); - } - - var d = new Date(); - var konamiGreetings = new Array("Suze Sunday", "Malibu Monday", "Tequila Tuesday", "Whiskey Wednesday", "Negroni Day", "Fernet Friday", "Sancerre Saturday"); - var konamiMode = $cookies.konamiLogin; - if (konamiMode == "1") { - $scope.greeting = "Happy " + konamiGreetings[d.getDay()]; - } else { - localizationService.localize("login_greeting" + d.getDay()).then(function (label) { - $scope.greeting = label; - }); // weekday[d.getDay()]; - } - $scope.errorMsg = ""; - - $scope.externalLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl; - $scope.externalLoginProviders = externalLoginInfo.providers; - $scope.externalLoginInfo = externalLoginInfo; - $scope.resetPasswordCodeInfo = resetPasswordCodeInfo; - $scope.backgroundImage = Umbraco.Sys.ServerVariables.umbracoSettings.loginBackgroundImage; - - $scope.activateKonamiMode = function () { - if ($cookies.konamiLogin == "1") { - // somehow I can't update the cookie value using $cookies, so going native - document.cookie = "konamiLogin=; expires=Thu, 01 Jan 1970 00:00:01 GMT;"; - document.location.reload(); - } else { - document.cookie = "konamiLogin=1; expires=Tue, 01 Jan 2030 00:00:01 GMT;"; - $scope.$apply(function () { - $scope.greeting = "Happy " + konamiGreetings[d.getDay()]; - }); - } - } - - $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, - // so if they're not empty, we'll just make sure to set them to valid. - if (login && password && login.length > 0 && password.length > 0) { - $scope.loginForm.username.$setValidity('auth', true); - $scope.loginForm.password.$setValidity('auth', true); - } - - if ($scope.loginForm.$invalid) { - return; - } - - userService.authenticate(login, password) - .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; - - //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 - // be submitted again. - $scope.loginForm.username.$viewChangeListeners.push(function () { - if ($scope.loginForm.username.$invalid) { - $scope.loginForm.username.$setValidity('auth', true); - } - }); - $scope.loginForm.password.$viewChangeListeners.push(function () { - if ($scope.loginForm.password.$invalid) { - $scope.loginForm.password.$setValidity('auth', true); - } - }); - }; - - $scope.requestPasswordResetSubmit = function (email) { - - if (email && email.length > 0) { - $scope.requestPasswordResetForm.email.$setValidity('auth', true); - } - - $scope.showEmailResetConfirmation = false; - - if ($scope.requestPasswordResetForm.$invalid) { - return; - } - - $scope.errorMsg = ""; - - authResource.performRequestPasswordReset(email) - .then(function () { - //remove the email entered - $scope.email = ""; - $scope.showEmailResetConfirmation = true; - }, function (reason) { - $scope.errorMsg = reason.errorMsg; - $scope.requestPasswordResetForm.email.$setValidity("auth", false); - }); - - $scope.requestPasswordResetForm.email.$viewChangeListeners.push(function () { - if ($scope.requestPasswordResetForm.email.$invalid) { - $scope.requestPasswordResetForm.email.$setValidity('auth', true); - } - }); - }; - - $scope.setPasswordSubmit = function (password, confirmPassword) { - - $scope.showSetPasswordConfirmation = false; - - if (password && confirmPassword && password.length > 0 && confirmPassword.length > 0) { - $scope.setPasswordForm.password.$setValidity('auth', true); - $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); - } - - if ($scope.setPasswordForm.$invalid) { - return; - } - - authResource.performSetPassword($scope.resetPasswordCodeInfo.resetCodeModel.userId, password, confirmPassword, $scope.resetPasswordCodeInfo.resetCodeModel.resetCode) - .then(function () { - $scope.showSetPasswordConfirmation = true; - $scope.resetComplete = true; - - //reset the values in the resetPasswordCodeInfo angular so if someone logs out the change password isn't shown again - resetPasswordCodeInfo.resetCodeModel = null; - - }, function (reason) { - if (reason.data && reason.data.Message) { - $scope.errorMsg = reason.data.Message; - } - else { - $scope.errorMsg = reason.errorMsg; - } - $scope.setPasswordForm.password.$setValidity("auth", false); - $scope.setPasswordForm.confirmPassword.$setValidity("auth", false); - }); - - $scope.setPasswordForm.password.$viewChangeListeners.push(function () { - if ($scope.setPasswordForm.password.$invalid) { - $scope.setPasswordForm.password.$setValidity('auth', true); - } - }); - $scope.setPasswordForm.confirmPassword.$viewChangeListeners.push(function () { - if ($scope.setPasswordForm.confirmPassword.$invalid) { - $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); - } - }); + }); + $scope.setPasswordForm.confirmPassword.$viewChangeListeners.push(function () { + if ($scope.setPasswordForm.confirmPassword.$invalid) { + $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); } + }); + } - //Now, show the correct panel: + //Now, show the correct panel: - if ($scope.resetPasswordCodeInfo.resetCodeModel) { - $scope.showSetPassword(); - } - else if ($scope.resetPasswordCodeInfo.errors.length > 0) { - $scope.view = "password-reset-code-expired"; - } - else { - $scope.showLogin(); - } + if ($scope.resetPasswordCodeInfo.resetCodeModel) { + $scope.showSetPassword(); + } + else if ($scope.resetPasswordCodeInfo.errors.length > 0) { + $scope.view = "password-reset-code-expired"; + } + else { + $scope.showLogin(); + } - init(); + init(); - }); + }); 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 49b4cadc0d..42d977070d 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 @@ -1,202 +1,201 @@ 
-
- - +
- + - - - + + + + + +
\ No newline at end of file diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 61278a1361..64be61d862 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -49,26 +49,39 @@ namespace Umbraco.Web.Editors { get { return _signInManager ?? (_signInManager = TryGetOwinContext().Result.GetBackOfficeSignInManager()); } } - + /// - /// Checks if a valid token is specified for an invited user and if so returns the user object + /// Checks if a valid token is specified for an invited user and if so logs the user in and returns the user object /// + /// /// /// + /// + /// This will also update the security stamp for the user so it can only be used once + /// [ValidateAngularAntiForgeryToken] - public UserDisplay PostVerifyInvite([FromUri]string token) + public async Task PostVerifyInvite([FromUri]int id, [FromUri]string token) { if (string.IsNullOrWhiteSpace(token)) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); var decoded = token.FromUrlBase64(); if (decoded.IsNullOrWhiteSpace()) - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - var user = Services.UserService.ValidateInviteToken(decoded); + var user = await UserManager.FindByIdAsync(id); if (user == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + var result = await UserManager.ConfirmEmailAsync(id, decoded); + + if (result.Succeeded == false) + { + throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse(string.Join(", ", result.Errors))); + } + + await SignInManager.SignInAsync(user, false, false); + return Mapper.Map(user); } diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index dac443c330..1114a068c8 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -253,6 +253,8 @@ namespace Umbraco.Web.Editors /// public async Task PostInviteUser(UserInvite userSave) { + //TODO: Allow re-inviting the user! + if (userSave == null) throw new ArgumentNullException("userSave"); if (ModelState.IsValid == false) @@ -267,19 +269,57 @@ namespace Umbraco.Web.Editors Request.CreateNotificationValidationErrorResponse("No Email server is configured")); } - var existing = Services.UserService.GetByEmail(userSave.Email); - if (existing != null) + var identityUser = await UserManager.FindByEmailAsync(userSave.Email); + if (identityUser != null && identityUser.LastLoginDateUtc.HasValue && identityUser.LastLoginDateUtc.Value == default(DateTime)) { ModelState.AddModelError("Email", "A user with the email already exists"); throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); } - var user = Mapper.Map(userSave); + if (identityUser == null) + { + identityUser = new BackOfficeIdentityUser + { + Email = userSave.Email, + Name = userSave.Name, + UserName = userSave.Email + }; + + //Save the user first + var result = await UserManager.CreateAsync(identityUser); + if (result.Succeeded == false) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse(string.Join(", ", result.Errors))); + } + } + else + { + identityUser.Name = userSave.Name; + var result = await UserManager.UpdateAsync(identityUser); + if (result.Succeeded == false) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse(string.Join(", ", result.Errors))); + } + } + + //Add/Update to roles + var roleResult = await UserManager.AddToRolesAsync(identityUser.Id, userSave.UserGroups.ToArray()); + if (roleResult.Succeeded == false) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse(string.Join(", ", roleResult.Errors))); + } - var link = string.Format("{0}#/login/false?invite={1}", + //now send the email + var token = await UserManager.GenerateEmailConfirmationTokenAsync(identityUser.Id); + var link = string.Format("{0}#/login/false?invite={1}{2}{3}", ApplicationContext.UmbracoApplicationUrl, - user.SecurityStamp.ToUrlBase64()); - + identityUser.Id, + WebUtility.UrlEncode("|"), + token.ToUrlBase64()); + await UserManager.EmailService.SendAsync(new IdentityMessage { Body = string.Format("You have been invited to the Umbraco Back Office!\n\n{0}\n\nClick this link to accept the invite\n\n{1}", userSave.Message, link), @@ -287,11 +327,7 @@ namespace Umbraco.Web.Editors Subject = "You have been invited to the Umbraco Back Office!" }); - //Email was successful, so save the user now - - Services.UserService.Save(user); - - return Mapper.Map(user); + return Mapper.Map(identityUser); } /// diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 5a77511190..e559d6777d 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -16,31 +16,16 @@ using Umbraco.Core.Services; using UserProfile = Umbraco.Web.Models.ContentEditing.UserProfile; namespace Umbraco.Web.Models.Mapping -{ +{ + internal class UserModelMapper : MapperConfiguration { public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) { //Used for merging existing UserSave to an existing IUser instance - this will not create an IUser instance! config.CreateMap() + .IgnoreAllUnmapped() .ForMember(user => user.Language, expression => expression.MapFrom(save => save.Culture)) - .ForMember(user => user.Avatar, expression => expression.Ignore()) - .ForMember(user => user.SessionTimeout, expression => expression.Ignore()) - .ForMember(user => user.SecurityStamp, expression => expression.Ignore()) - .ForMember(user => user.ProviderUserKey, expression => expression.Ignore()) - .ForMember(user => user.RawPasswordValue, expression => expression.Ignore()) - .ForMember(user => user.PasswordQuestion, expression => expression.Ignore()) - .ForMember(user => user.RawPasswordAnswerValue, expression => expression.Ignore()) - .ForMember(user => user.Comments, expression => expression.Ignore()) - .ForMember(user => user.IsApproved, expression => expression.Ignore()) - .ForMember(user => user.IsLockedOut, expression => expression.Ignore()) - .ForMember(user => user.LastLoginDate, expression => expression.Ignore()) - .ForMember(user => user.LastPasswordChangeDate, expression => expression.Ignore()) - .ForMember(user => user.LastLockoutDate, expression => expression.Ignore()) - .ForMember(user => user.FailedPasswordAttempts, expression => expression.Ignore()) - .ForMember(user => user.DeletedDate, expression => expression.Ignore()) - .ForMember(user => user.CreateDate, expression => expression.Ignore()) - .ForMember(user => user.UpdateDate, expression => expression.Ignore()) .AfterMap((save, user) => { user.ClearGroups(); @@ -53,31 +38,11 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ConstructUsing(invite => new User(invite.Name, invite.Email, invite.Email, Guid.NewGuid().ToString("N"))) - //generate a token for the invite + .IgnoreAllUnmapped() + //generate a new token .ForMember(user => user.SecurityStamp, expression => expression.MapFrom(x => (DateTime.Now + x.Email).ToSHA1())) //all invited users will not be approved, completing the invite will approve the user - .ForMember(user => user.IsApproved, expression => expression.UseValue(false)) - .ForMember(user => user.Id, expression => expression.Ignore()) - .ForMember(user => user.Avatar, expression => expression.Ignore()) - .ForMember(user => user.SessionTimeout, expression => expression.Ignore()) - .ForMember(user => user.StartContentIds, expression => expression.Ignore()) - .ForMember(user => user.StartMediaIds, expression => expression.Ignore()) - .ForMember(user => user.Language, expression => expression.Ignore()) - .ForMember(user => user.ProviderUserKey, expression => expression.Ignore()) - .ForMember(user => user.Username, expression => expression.Ignore()) - .ForMember(user => user.RawPasswordValue, expression => expression.Ignore()) - .ForMember(user => user.PasswordQuestion, expression => expression.Ignore()) - .ForMember(user => user.RawPasswordAnswerValue, expression => expression.Ignore()) - .ForMember(user => user.Comments, expression => expression.Ignore()) - .ForMember(user => user.IsApproved, expression => expression.Ignore()) - .ForMember(user => user.IsLockedOut, expression => expression.Ignore()) - .ForMember(user => user.LastLoginDate, expression => expression.Ignore()) - .ForMember(user => user.LastPasswordChangeDate, expression => expression.Ignore()) - .ForMember(user => user.LastLockoutDate, expression => expression.Ignore()) - .ForMember(user => user.FailedPasswordAttempts, expression => expression.Ignore()) - .ForMember(user => user.DeletedDate, expression => expression.Ignore()) - .ForMember(user => user.CreateDate, expression => expression.Ignore()) - .ForMember(user => user.UpdateDate, expression => expression.Ignore()) + .ForMember(user => user.IsApproved, expression => expression.UseValue(false)) .AfterMap((invite, user) => { user.ClearGroups();