From d232aa1f00b5f545531d4b883f9d7362a050b3fa Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 25 Jul 2017 20:51:58 +1000 Subject: [PATCH] U4-10175 User avatar should refresh in top left corner when it's changed --- .../common/security/securityinterceptor.js | 8 +- .../src/common/services/events.service.js | 5 +- .../src/common/services/user.service.js | 20 +++ .../src/controllers/main.controller.js | 22 +++- .../Editors/AuthenticationController.cs | 2 +- .../Editors/CurrentUserController.cs | 1 + src/Umbraco.Web/Editors/UsersController.cs | 2 + src/Umbraco.Web/Umbraco.Web.csproj | 3 +- .../AppendUserModifiedHeaderAttribute.cs | 76 +++++++++++ .../CheckIfUserTicketDataIsStaleAttribute.cs | 121 ++++++++++++++++++ .../VerifyIfUserTicketDataIsStaleAttribute.cs | 117 ----------------- .../WebApi/UmbracoAuthorizedApiController.cs | 2 +- 12 files changed, 256 insertions(+), 123 deletions(-) create mode 100644 src/Umbraco.Web/WebApi/Filters/AppendUserModifiedHeaderAttribute.cs create mode 100644 src/Umbraco.Web/WebApi/Filters/CheckIfUserTicketDataIsStaleAttribute.cs delete mode 100644 src/Umbraco.Web/WebApi/Filters/VerifyIfUserTicketDataIsStaleAttribute.cs diff --git a/src/Umbraco.Web.UI.Client/src/common/security/securityinterceptor.js b/src/Umbraco.Web.UI.Client/src/common/security/securityinterceptor.js index eb16279815..6f5712a78d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/security/securityinterceptor.js +++ b/src/Umbraco.Web.UI.Client/src/common/security/securityinterceptor.js @@ -1,6 +1,6 @@ angular.module('umbraco.security.interceptor') // This http interceptor listens for authentication successes and failures - .factory('securityInterceptor', ['$injector', 'securityRetryQueue', 'notificationsService', 'requestInterceptorFilter', function ($injector, queue, notifications, requestInterceptorFilter) { + .factory('securityInterceptor', ['$injector', 'securityRetryQueue', 'notificationsService', 'eventsService', 'requestInterceptorFilter', function ($injector, queue, notifications, eventsService, requestInterceptorFilter) { return function(promise) { return promise.then( @@ -16,6 +16,12 @@ angular.module('umbraco.security.interceptor') userService.setUserTimeout(headers["x-umb-user-seconds"]); } + //this checks if the user's values have changed, in which case we need to update the user details throughout + //the back office similar to how we do when a user logs in + if (headers["x-umb-user-modified"]) { + eventsService.emit("app.userRefresh"); + } + return promise; }, function(originalResponse) { // Intercept failed requests diff --git a/src/Umbraco.Web.UI.Client/src/common/services/events.service.js b/src/Umbraco.Web.UI.Client/src/common/services/events.service.js index 5c1f606614..174cc8abe2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/events.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/events.service.js @@ -6,7 +6,10 @@ app.ready app.authenticated app.notAuthenticated - app.closeDialogs + app.closeDialogs + app.ysod + app.reInitialize + app.userRefresh */ function eventsService($q, $rootScope) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js index 5728df335c..f20c3df44f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js @@ -225,6 +225,26 @@ angular.module('umbraco.services') }); }, + /** Refreshes the current user data with the data stored for the user on the server and returns it */ + refreshCurrentUser: function() { + var deferred = $q.defer(); + + authResource.getCurrentUser() + .then(function (data) { + + var result = { user: data, authenticated: true, lastUserId: lastUserId, loginType: "implicit" }; + + setCurrentUser(data); + + deferred.resolve(currentUser); + }, function () { + //it failed, so they are not logged in + deferred.reject(); + }); + + return deferred.promise; + }, + /** Returns the current user object in a promise */ getCurrentUser: function (args) { var deferred = $q.defer(); diff --git a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js b/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js index f17fefcac8..dc39678a2f 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js @@ -54,7 +54,27 @@ function MainController($scope, $rootScope, $location, $routeParams, $timeout, $ $scope.user = null; })); - //when the app is read/user is logged in, setup the data + evts.push(eventsService.on("app.userRefresh", function(evt) { + userService.refreshCurrentUser().then(function(data) { + $scope.user = data; + + //Load locale file + if ($scope.user.locale) { + tmhDynamicLocale.set($scope.user.locale); + } + + if ($scope.user.avatars) { + $scope.avatar = []; + if (angular.isArray($scope.user.avatars)) { + for (var i = 0; i < $scope.user.avatars.length; i++) { + $scope.avatar.push({ value: $scope.user.avatars[i] }); + } + } + } + }); + })); + + //when the app is ready/user is logged in, setup the data evts.push(eventsService.on("app.ready", function (evt, data) { $scope.authenticated = data.authenticated; diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index f61e11a791..aed959e291 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -150,7 +150,7 @@ namespace Umbraco.Web.Editors /// [WebApi.UmbracoAuthorize] [SetAngularAntiForgeryTokens] - [VerifyIfUserTicketDataIsStale] + [CheckIfUserTicketDataIsStale] public UserDetail GetCurrentUser() { var user = UmbracoContext.Security.CurrentUser; diff --git a/src/Umbraco.Web/Editors/CurrentUserController.cs b/src/Umbraco.Web/Editors/CurrentUserController.cs index 8db46ec1e8..ee204bc2cd 100644 --- a/src/Umbraco.Web/Editors/CurrentUserController.cs +++ b/src/Umbraco.Web/Editors/CurrentUserController.cs @@ -72,6 +72,7 @@ namespace Umbraco.Web.Editors return userDisplay; } + [AppendUserModifiedHeader] [FileUploadCleanupFilter(false)] public async Task PostSetAvatar() { diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 0039128e86..ab7932b82e 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -78,6 +78,7 @@ namespace Umbraco.Web.Editors return urls; } + [AppendUserModifiedHeader("id")] [FileUploadCleanupFilter(false)] public async Task PostSetAvatar(int id) { @@ -141,6 +142,7 @@ namespace Umbraco.Web.Editors return request.CreateResponse(HttpStatusCode.OK, user.GetCurrentUserAvatarUrls(userService, staticCache)); } + [AppendUserModifiedHeader("id")] public HttpResponseMessage PostClearAvatar(int id) { var found = Services.UserService.GetUserById(id); diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index ed053b13e7..5b17d94be3 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -783,6 +783,7 @@ + @@ -797,7 +798,7 @@ - + diff --git a/src/Umbraco.Web/WebApi/Filters/AppendUserModifiedHeaderAttribute.cs b/src/Umbraco.Web/WebApi/Filters/AppendUserModifiedHeaderAttribute.cs new file mode 100644 index 0000000000..77fee75693 --- /dev/null +++ b/src/Umbraco.Web/WebApi/Filters/AppendUserModifiedHeaderAttribute.cs @@ -0,0 +1,76 @@ +using System; +using System.Web.Http.Filters; +using Umbraco.Core; +using Umbraco.Core.Models; + +namespace Umbraco.Web.WebApi.Filters +{ + /// + /// Appends a custom response header to notify the UI that the current user data has been modified + /// + public sealed class AppendUserModifiedHeaderAttribute : ActionFilterAttribute + { + private readonly string _userIdParameter; + + /// + /// An empty constructor which will always set the header + /// + public AppendUserModifiedHeaderAttribute() + { + } + + /// + /// A constructor specifying the action parameter name containing the user id to match against the current user and if they match the header will be appended + /// + /// + public AppendUserModifiedHeaderAttribute(string userIdParameter) + { + if (userIdParameter == null) throw new ArgumentNullException("userIdParameter"); + _userIdParameter = userIdParameter; + } + + public static void AppendHeader(HttpActionExecutedContext actionExecutedContext) + { + if (actionExecutedContext.Response.Headers.Contains("X-Umb-User-Modified") == false) + { + actionExecutedContext.Response.Headers.Add("X-Umb-User-Modified", "1"); + } + } + + public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) + { + base.OnActionExecuted(actionExecutedContext); + + if (_userIdParameter.IsNullOrWhiteSpace()) + { + AppendHeader(actionExecutedContext); + } + else + { + var actionContext = actionExecutedContext.ActionContext; + if (actionContext.ActionArguments[_userIdParameter] == null) + { + throw new InvalidOperationException("No argument found for the current action with the name: " + _userIdParameter); + } + + var user = UmbracoContext.Current.Security.CurrentUser; + if (user == null) return; + + var userId = GetUserIdFromParameter(actionContext.ActionArguments[_userIdParameter]); + + if (userId == user.Id) + AppendHeader(actionExecutedContext); + } + + } + + private int GetUserIdFromParameter(object parameterValue) + { + if (parameterValue is int) + { + return (int)parameterValue; + } + throw new InvalidOperationException("The id type: " + parameterValue.GetType() + " is not a supported id"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/CheckIfUserTicketDataIsStaleAttribute.cs b/src/Umbraco.Web/WebApi/Filters/CheckIfUserTicketDataIsStaleAttribute.cs new file mode 100644 index 0000000000..46c36e9acd --- /dev/null +++ b/src/Umbraco.Web/WebApi/Filters/CheckIfUserTicketDataIsStaleAttribute.cs @@ -0,0 +1,121 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; +using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Security; +using Umbraco.Web.Security; +using UserExtensions = Umbraco.Core.Models.UserExtensions; + +namespace Umbraco.Web.WebApi.Filters +{ + /// + /// This filter will check if the current Principal/Identity assigned to the request has stale data in it compared + /// to what is persisted for the current user and will update the current auth ticket with the correct data if required and output + /// a custom response header for the UI to be notified of it. + /// + public sealed class CheckIfUserTicketDataIsStaleAttribute : ActionFilterAttribute + { + public override async Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken) + { + await CheckStaleData(actionContext); + } + + public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken) + { + await CheckStaleData(actionExecutedContext.ActionContext); + + //we need new tokens and append the custom header if changes have been made + if (actionExecutedContext.ActionContext.Request.Properties.ContainsKey(typeof(CheckIfUserTicketDataIsStaleAttribute).Name)) + { + var tokenFilter = new SetAngularAntiForgeryTokensAttribute(); + tokenFilter.OnActionExecuted(actionExecutedContext); + + //add the header + AppendUserModifiedHeaderAttribute.AppendHeader(actionExecutedContext); + } + } + + private async Task CheckStaleData(HttpActionContext actionContext) + { + if (actionContext == null + || actionContext.Request == null + || actionContext.RequestContext == null + || actionContext.RequestContext.Principal == null + || actionContext.RequestContext.Principal.Identity == null) + { + return; + } + + //don't execute if it's already been done + if (actionContext.Request.Properties.ContainsKey(typeof(CheckIfUserTicketDataIsStaleAttribute).Name)) + return; + + var identity = actionContext.RequestContext.Principal.Identity as UmbracoBackOfficeIdentity; + if (identity == null) return; + + var userId = identity.Id.TryConvertTo(); + if (userId == false) return; + + var user = ApplicationContext.Current.Services.UserService.GetUserById(userId.Result); + if (user == null) return; + + //a list of checks to execute, if any of them pass then we resync + var checks = new Func[] + { + () => user.Username != identity.Username, + () => + { + var culture = UserExtensions.GetUserCulture(user, ApplicationContext.Current.Services.TextService); + return culture != null && culture.ToString() != identity.Culture; + }, + () => user.AllowedSections.UnsortedSequenceEqual(identity.AllowedApplications) == false, + () => user.Groups.Select(x => x.Alias).UnsortedSequenceEqual(identity.Roles) == false, + () => + { + var startContentIds = UserExtensions.CalculateContentStartNodeIds(user, ApplicationContext.Current.Services.EntityService); + return startContentIds.UnsortedSequenceEqual(identity.StartContentNodes) == false; + }, + () => + { + var startMediaIds = UserExtensions.CalculateMediaStartNodeIds(user, ApplicationContext.Current.Services.EntityService); + return startMediaIds.UnsortedSequenceEqual(identity.StartMediaNodes) == false; + } + }; + + if (checks.Any(check => check())) + { + await ReSync(user, actionContext); + } + } + + /// + /// This will update the current request IPrincipal to be correct and re-create the auth ticket + /// + /// + /// + /// + private async Task ReSync(IUser user, HttpActionContext actionContext) + { + var owinCtx = actionContext.Request.TryGetOwinContext(); + if (owinCtx) + { + var signInManager = owinCtx.Result.GetBackOfficeSignInManager(); + + //ensure the remainder of the request has the correct principal set + actionContext.Request.SetPrincipalForRequest(user); + + var backOfficeIdentityUser = Mapper.Map(user); + await signInManager.SignInAsync(backOfficeIdentityUser, isPersistent: true, rememberBrowser: false); + + //flag that we've made changes + actionContext.Request.Properties[typeof(CheckIfUserTicketDataIsStaleAttribute).Name] = true; + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/VerifyIfUserTicketDataIsStaleAttribute.cs b/src/Umbraco.Web/WebApi/Filters/VerifyIfUserTicketDataIsStaleAttribute.cs deleted file mode 100644 index be2d726caf..0000000000 --- a/src/Umbraco.Web/WebApi/Filters/VerifyIfUserTicketDataIsStaleAttribute.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using System.Web.Http.Controllers; -using System.Web.Http.Filters; -using AutoMapper; -using Umbraco.Core; -using Umbraco.Core.Models.Identity; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Security; -using Umbraco.Web.Security; -using UserExtensions = Umbraco.Core.Models.UserExtensions; - -namespace Umbraco.Web.WebApi.Filters -{ - - /// - /// This filter will check if the current Principal/Identity assigned to the request has stale data in it compared - /// to what is persisted for the current user and will update the current auth ticket with the correct data if required. - /// - public sealed class VerifyIfUserTicketDataIsStaleAttribute : ActionFilterAttribute - { - public override async Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken) - { - await CheckStaleData(actionContext); - } - - public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken) - { - await CheckStaleData(actionExecutedContext.ActionContext); - - //lastly we need new tokens if changes have been made - if (actionExecutedContext.ActionContext.Request.Properties.ContainsKey(typeof(VerifyIfUserTicketDataIsStaleAttribute).Name)) - { - var tokenFilter = new SetAngularAntiForgeryTokensAttribute(); - tokenFilter.OnActionExecuted(actionExecutedContext); - } - } - - private async Task CheckStaleData(HttpActionContext actionContext) - { - //don't execute if it's already been done - if (actionContext.Request.Properties.ContainsKey(typeof(VerifyIfUserTicketDataIsStaleAttribute).Name)) - return; - - var identity = actionContext.RequestContext.Principal.Identity as UmbracoBackOfficeIdentity; - if (identity == null) return; - - var userId = identity.Id.TryConvertTo(); - if (userId == false) return; - - var user = ApplicationContext.Current.Services.UserService.GetUserById(userId.Result); - if (user == null) return; - - if (user.Username != identity.Username) - { - await ReSync(user, actionContext); - return; - } - - var culture = UserExtensions.GetUserCulture(user, ApplicationContext.Current.Services.TextService).ToString(); - if (culture != identity.Culture) - { - //TODO: Might have to log out if this happens or somehow refresh the back office UI with a special header maybe? - await ReSync(user, actionContext); - return; - } - - if (user.AllowedSections.UnsortedSequenceEqual(identity.AllowedApplications) == false) - { - await ReSync(user, actionContext); - return; - } - - if (user.Groups.Select(x => x.Alias).UnsortedSequenceEqual(identity.Roles) == false) - { - await ReSync(user, actionContext); - return; - } - - var startContentIds = UserExtensions.CalculateContentStartNodeIds(user, ApplicationContext.Current.Services.EntityService); - if (startContentIds.UnsortedSequenceEqual(identity.StartContentNodes) == false) - { - await ReSync(user, actionContext); - return; - } - - var startMediaIds = UserExtensions.CalculateMediaStartNodeIds(user, ApplicationContext.Current.Services.EntityService); - if (startMediaIds.UnsortedSequenceEqual(identity.StartMediaNodes) == false) - { - await ReSync(user, actionContext); - return; - } - } - - /// - /// This will update the current request IPrincipal to be correct and re-create the auth ticket - /// - /// - /// - /// - private async Task ReSync(IUser user, HttpActionContext actionContext) - { - var owinCtx = actionContext.Request.TryGetOwinContext().Result; - var signInManager = owinCtx.GetBackOfficeSignInManager(); - - //ensure the remainder of the request has the correct principal set - actionContext.Request.SetPrincipalForRequest(user); - - var backOfficeIdentityUser = Mapper.Map(user); - await signInManager.SignInAsync(backOfficeIdentityUser, isPersistent: true, rememberBrowser: false); - - //flag that we've made changes - actionContext.Request.Properties[typeof(VerifyIfUserTicketDataIsStaleAttribute).Name] = true; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs index 430fd01e19..25f85def0f 100644 --- a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs +++ b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web.WebApi [UmbracoAuthorize] [DisableBrowserCache] [UmbracoWebApiRequireHttps] - [VerifyIfUserTicketDataIsStale] + [CheckIfUserTicketDataIsStale] public abstract class UmbracoAuthorizedApiController : UmbracoApiController {