Includes nice social buttons, updates styling on login and user panel, updates logic to un-link accounts
This commit is contained in:
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user