Includes nice social buttons, updates styling on login and user panel, updates logic to un-link accounts

This commit is contained in:
Shannon
2015-02-22 13:29:00 +01:00
parent afa4c7b697
commit d9cf9cee88
10 changed files with 165 additions and 91 deletions

View File

@@ -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}"

View File

@@ -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

View File

@@ -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%); }
.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;
}

View File

@@ -36,11 +36,14 @@
<div ng-repeat="login in externalLoginProviders">
<button type="submit" class="btn btn-default" id="{{login.authType}}" name="provider" value="{{login.authType}}"
<button type="submit" class="btn btn-block btn-large btn-social"
ng-class="login.properties.SocialStyle"
id="{{login.authType}}" name="provider" value="{{login.authType}}"
title="Log in using your {{login.caption}} account">
{{login.authType}}
<i class="fa" ng-class="login.properties.SocialIcon"></i>
Sign in with {{login.caption}}
</button>
</div>
</form>
</div>

View File

@@ -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 () {

View File

@@ -1,4 +1,4 @@
<div class="umb-panel" ng-controller="Umbraco.Dialogs.UserController">
<div class="umb-panel umb-user-panel" ng-controller="Umbraco.Dialogs.UserController">
<div class="umb-panel-header">
<div class="umb-el-wrap umb-panel-buttons">
<div class="btn-toolbar umb-btn-toolbar">
@@ -30,30 +30,35 @@
</p>
</div>
<div class="umb-pane">
<div class="umb-pane external-logins">
<h5>External login providers</h5>
<div ng-repeat="login in externalLoginProviders" class="alert alert-info">
<div ng-repeat="login in externalLoginProviders">
<form ng-if="login.linkedProviderKey == undefined" method="POST" name="externalLoginForm" action="{{externalLinkLoginFormAction}}">
<button class="btn btn-default icon-link"
<form ng-if="login.linkedProviderKey == undefined" method="POST" name="externalLoginForm"
action="{{externalLinkLoginFormAction}}">
<button class="btn btn-block btn-social"
ng-class="login.properties.SocialStyle"
id="{{login.authType}}"
name="provider"
value="{{login.authType}}"
title="Link your user account to your {{login.caption}} account"></button>
value="{{login.authType}}">
<i class="fa" ng-class="login.properties.SocialIcon"></i>
Link your {{login.caption}} account
</button>
</form>
<button ng-if="login.linkedProviderKey != undefined"
ng-click="unlink($event, login.authType, login.linkedProviderKey)"
class="btn btn-default icon-remove"
class="btn btn-block btn-social"
ng-class="login.properties.SocialStyle"
id="{{login.authType}}"
name="provider"
value="{{login.authType}}"
title="Un-link your user account from your {{login.caption}} account"></button>
<span>{{login.caption}}</span>
value="{{login.authType}}">
<i class="fa" ng-class="login.properties.SocialIcon"></i>
Un-link your {{login.caption}} account
</button>
</div>
</div>

View File

@@ -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)

View File

@@ -28,8 +28,13 @@
<title ng-bind="$root.locationTitle">Umbraco</title>
@{ 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)))

View File

@@ -139,7 +139,7 @@ namespace Umbraco.Web.Editors
/// </remarks>
[WebApi.UmbracoAuthorize]
[SetAngularAntiForgeryTokens]
public async Task<UserDetail> GetCurrentUser()
public UserDetail GetCurrentUser()
{
var user = Services.UserService.GetUserById(UmbracoContext.Security.GetUserId());
var result = Mapper.Map<UserDetail>(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<Dictionary<string, string>> 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

View File

@@ -43,7 +43,6 @@ namespace Umbraco.Web.Models.ContentEditing
[DataMember(Name = "allowedSections")]
public IEnumerable<string> AllowedSections { get; set; }
[DataMember(Name = "linkedLogins")]
public IEnumerable<KeyValuePair<string, string>> LinkedLogins { get; set; }
}
}