From d9cf9cee88dbf15e1657241210ae3c7a1a76b6fc Mon Sep 17 00:00:00 2001 From: Shannon Date: Sun, 22 Feb 2015 13:29:00 +0100 Subject: [PATCH] Includes nice social buttons, updates styling on login and user panel, updates logic to un-link accounts --- src/Umbraco.Web.UI.Client/bower.json | 12 ++- .../src/common/resources/auth.resource.js | 10 ++ src/Umbraco.Web.UI.Client/src/less/panel.less | 9 +- .../src/views/common/dialogs/login.html | 9 +- .../views/common/dialogs/user.controller.js | 57 +++++----- .../src/views/common/dialogs/user.html | 31 +++--- src/Umbraco.Web.UI/App_Code/OwinStartup.cs | 100 +++++++++++------- .../umbraco/Views/Default.cshtml | 9 +- .../Editors/AuthenticationController.cs | 16 +-- .../Models/ContentEditing/UserDetail.cs | 3 +- 10 files changed, 165 insertions(+), 91 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index e8ad94653f..ce312d669a 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -19,6 +19,7 @@ "typeahead.js": "~0.10.5", "underscore": "~1.7.0", "rgrove-lazyload": "*", + "bootstrap-social": "~4.8.0" "jquery": "2.0.3", "jquery-file-upload": "~9.4.0", "jquery-ui": "1.10.3", @@ -34,8 +35,15 @@ "underscore": { "": "underscore-min.{js,map}" }, - "angular-dynamic-locale": { - "": "tmhDynamicLocale.min.{js,js.map}" + "bootstrap-social": { + "": "bootstrap-social.css" + }, + "font-awesome": { + "css": "css/font-awesome.min.css", + "fonts" : "fonts/*" + }, + "bootstrap": { + "ignore": "*.ignore" }, "jquery": { "": "jquery.min.{js,map}" 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 973b841cdd..f32602bda6 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 @@ -123,6 +123,16 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { "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 diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index 7fc2d061ea..62a5d21ac7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -280,4 +280,11 @@ .umb-dialog a.text-success:hover, .umb-dialog a.text-success:focus, .umb-panel a.text-success:hover, -.umb-panel a.text-success:focus { color: darken(@formSuccessText, 10%); } \ No newline at end of file +.umb-panel a.text-success:focus { color: darken(@formSuccessText, 10%); } + +.umb-user-panel .external-logins form { + margin:0; +} +.umb-user-panel .external-logins button { + margin:5px; +} 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 4981b4ca12..dd7971b678 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 @@ -36,11 +36,14 @@
- - +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js index c6531e6016..6048edfa95 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js @@ -51,7 +51,37 @@ angular.module("umbraco") }, 1000, false); // 1 second, do NOT execute a global digest } - $scope.unlink = function(e, loginProvider, providerKey) { + function updateUserInfo() { + //get the user + userService.getCurrentUser().then(function (user) { + $scope.user = user; + if ($scope.user) { + $scope.remainingAuthSeconds = $scope.user.remainingAuthSeconds; + $scope.canEditProfile = _.indexOf($scope.user.allowedSections, "users") > -1; + //set the timer + updateTimeout(); + + authResource.getCurrentUserLinkedLogins().then(function(logins) { + //reset all to be un-linked + for (var provider in $scope.externalLoginProviders) { + $scope.externalLoginProviders[provider].linkedProviderKey = undefined; + } + + //set the linked logins + for (var login in logins) { + var found = _.find($scope.externalLoginProviders, function (i) { + return i.authType == login; + }); + if (found) { + found.linkedProviderKey = logins[login]; + } + } + }); + } + }); + } + + $scope.unlink = function (e, loginProvider, providerKey) { var result = confirm("Are you sure you want to unlink this account?"); if (!result) { e.preventDefault(); @@ -59,32 +89,11 @@ angular.module("umbraco") } authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) { - var asdf = ";" - }, function(err) { - var asdf = err; + updateUserInfo(); }); } - //get the user - userService.getCurrentUser().then(function (user) { - $scope.user = user; - if ($scope.user) { - $scope.remainingAuthSeconds = $scope.user.remainingAuthSeconds; - $scope.canEditProfile = _.indexOf($scope.user.allowedSections, "users") > -1; - //set the timer - updateTimeout(); - - //set the linked logins - for (var login in $scope.user.linkedLogins) { - var found = _.find($scope.externalLoginProviders, function(i) { - return i.authType == login; - }); - if (found) { - found.linkedProviderKey = $scope.user.linkedLogins[login]; - } - } - } - }); + updateUserInfo(); //remove all event handlers $scope.$on('$destroy', function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html index c73de2bb3a..37751bfab6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html @@ -1,4 +1,4 @@ -
+
@@ -30,30 +30,35 @@

-
+
External login providers
-
+
-
- + value="{{login.authType}}"> + + Link your {{login.caption}} account +
- - {{login.caption}} - + value="{{login.authType}}"> + + Un-link your {{login.caption}} account +
diff --git a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs index a4eb25b341..930aed24b0 100644 --- a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using System.Web; using Microsoft.IdentityModel.Clients.ActiveDirectory; using Microsoft.Owin; +using Microsoft.Owin.Security.Google; using Microsoft.Owin.Security.OpenIdConnect; using Owin; using Umbraco.Core; @@ -73,59 +74,84 @@ namespace Umbraco.Web.UI .UseUmbracoBackOfficeExternalCookieAuthentication(); //app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); - - + //app.UseGoogleAuthentication( + // clientId: "1072120697051-07jlhgrd5hodsfe7dgqimdie8qc1omet.apps.googleusercontent.com", + // clientSecret: "Ue9swN0lEX9rwxzQz1Y_tFzg"); + + var googleOptions = new GoogleOAuth2AuthenticationOptions + { + + }; + googleOptions.Description.Properties["SocialStyle"] = "btn-google-plus"; + googleOptions.Description.Properties["SocialIcon"] = "fa-google-plus"; + googleOptions.Caption = "Google"; + app.UseGoogleAuthentication(googleOptions); + + //AD docs are here: + // https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet + var authority = string.Format(CultureInfo.InvariantCulture, aadInstance, tenant); - app.UseOpenIdConnectAuthentication( - new OpenIdConnectAuthenticationOptions + var adOptions = new OpenIdConnectAuthenticationOptions + { + //NOTE: This by default is 'OpenIdConnect' but that doesn't match what identity actually stores in the + // loginProvider field in the database which is something like: https://sts.windows.net/1234.... + // which is something based on your AD setup. This value needs to match in order for accounts to detected as linked/un-linked + // in the back office. + AuthenticationType = "https://sts.windows.net/3bb0b4c5-364f-4394-ad36-0f29f95e5ddd/", + + ClientId = clientId, + Authority = authority, + PostLogoutRedirectUri = postLoginRedirectUri, + Notifications = new OpenIdConnectAuthenticationNotifications() { - ClientId = clientId, - Authority = authority, - PostLogoutRedirectUri = postLoginRedirectUri, - - Notifications = new OpenIdConnectAuthenticationNotifications() + // + // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away. + // + AuthorizationCodeReceived = (context) => { - // - // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away. - // - AuthorizationCodeReceived = (context) => - { - var code = context.Code; + var code = context.Code; - var credential = new ClientCredential(clientId, appKey); - var userObjectId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value; - var authContext = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId)); - AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode( - code, - //new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), - new Uri( - HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) + - HttpContext.Current.Request.RawUrl.EnsureStartsWith('/').EnsureEndsWith('/')), - credential, - graphResourceId); - - return Task.FromResult(0); - } + var credential = new ClientCredential(clientId, appKey); + var userObjectId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value; + var authContext = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId)); + AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode( + code, + //NOTE: This URL needs to match EXACTLY the same path that is configured in the AD + // configuration. + new Uri( + HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) + + HttpContext.Current.Request.RawUrl.EnsureStartsWith('/').EnsureEndsWith('/')), + credential, + graphResourceId); + return Task.FromResult(0); } - }); + } + + }; + adOptions.Description.Properties["SocialStyle"] = "btn-microsoft"; + adOptions.Description.Properties["SocialIcon"] = "fa-windows"; + adOptions.Caption = "Active Directory"; + app.UseOpenIdConnectAuthentication(adOptions); } } + //NOTE: Not sure exactly what this is for but it is found in the AD source demo: + // https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet/blob/master/TodoListWebApp/Utils/NaiveSessionCache.cs public class NaiveSessionCache : TokenCache { private static readonly object FileLock = new object(); - string UserObjectId = string.Empty; - string CacheId = string.Empty; + readonly string _userObjectId = string.Empty; + readonly string _cacheId = string.Empty; public NaiveSessionCache(string userId) { - UserObjectId = userId; - CacheId = UserObjectId + "_TokenCache"; + _userObjectId = userId; + _cacheId = _userObjectId + "_TokenCache"; this.AfterAccess = AfterAccessNotification; this.BeforeAccess = BeforeAccessNotification; @@ -136,7 +162,7 @@ namespace Umbraco.Web.UI { lock (FileLock) { - this.Deserialize((byte[])HttpContext.Current.Session[CacheId]); + this.Deserialize((byte[])HttpContext.Current.Session[_cacheId]); } } @@ -145,7 +171,7 @@ namespace Umbraco.Web.UI lock (FileLock) { // reflect changes in the persistent store - HttpContext.Current.Session[CacheId] = this.Serialize(); + HttpContext.Current.Session[_cacheId] = this.Serialize(); // once the write operation took place, restore the HasStateChanged bit to false this.HasStateChanged = false; } @@ -155,7 +181,7 @@ namespace Umbraco.Web.UI public override void Clear() { base.Clear(); - System.Web.HttpContext.Current.Session.Remove(CacheId); + System.Web.HttpContext.Current.Session.Remove(_cacheId); } public override void DeleteItem(TokenCacheItem item) diff --git a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml index c51e1c6ab9..a392942fe1 100644 --- a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml +++ b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml @@ -28,8 +28,13 @@ Umbraco - @{ Html.RequiresCss("assets/css/umbraco.css", "Umbraco");} - @{ Html.RequiresCss("tree/treeicons.css", "UmbracoClient");} + @{ + Html + .RequiresCss("assets/css/umbraco.css", "Umbraco") + .RequiresCss("tree/treeicons.css", "UmbracoClient") + .RequiresCss("lib/bootstrap-social/bootstrap-social.css", "Umbraco") + .RequiresCss("lib/font-awesome/css/font-awesome.min.css", "Umbraco"); + } @Html.RenderCssHere( new BasicPath("Umbraco", IOHelper.ResolveUrl(SystemDirectories.Umbraco)), new BasicPath("UmbracoClient", IOHelper.ResolveUrl(SystemDirectories.UmbracoClient))) diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index f6fcb2c0e7..a1e12401b1 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -139,7 +139,7 @@ namespace Umbraco.Web.Editors /// [WebApi.UmbracoAuthorize] [SetAngularAntiForgeryTokens] - public async Task GetCurrentUser() + public UserDetail GetCurrentUser() { var user = Services.UserService.GetUserById(UmbracoContext.Security.GetUserId()); var result = Mapper.Map(user); @@ -150,15 +150,17 @@ namespace Umbraco.Web.Editors result.SecondsUntilTimeout = httpContextAttempt.Result.GetRemainingAuthSeconds(); } - //now we need to fill in the user's linked logins, we can't do this in the mapper because it has no access to the - // user manager - - var identityUser = await UserManager.FindByIdAsync(user.Id); - result.LinkedLogins = identityUser.Logins.ToDictionary(x => x.LoginProvider, x => x.ProviderKey); - return result; } + [WebApi.UmbracoAuthorize] + [SetAngularAntiForgeryTokens] + public async Task> GetCurrentUserLinkedLogins() + { + var identityUser = await UserManager.FindByIdAsync(UmbracoContext.Security.GetUserId()); + return identityUser.Logins.ToDictionary(x => x.LoginProvider, x => x.ProviderKey); + } + private BackOfficeUserManager _userManager; protected BackOfficeUserManager UserManager diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs index 0d9d859b3d..d27736576f 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs @@ -43,7 +43,6 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "allowedSections")] public IEnumerable AllowedSections { get; set; } - [DataMember(Name = "linkedLogins")] - public IEnumerable> LinkedLogins { get; set; } + } } \ No newline at end of file