Browse Source

Profile page improvement.

pull/65/head
Sebastian Stehle 9 years ago
parent
commit
14d511e052
  1. 1
      src/Squidex.Infrastructure/Assets/FolderAssetStore.cs
  2. 5
      src/Squidex.Read.MongoDb/Users/WrappedIdentityUser.cs
  3. 32
      src/Squidex.Read/Users/ExternalLogin.cs
  4. 2
      src/Squidex.Read/Users/IUser.cs
  5. 2
      src/Squidex.Read/Users/UserExtensions.cs
  6. 4
      src/Squidex.Read/Users/UserManagerExtensions.cs
  7. 1
      src/Squidex/Controllers/Api/Users/UsersController.cs
  8. 12
      src/Squidex/Controllers/UI/Account/AccountController.cs
  9. 2
      src/Squidex/Controllers/UI/Account/LoginVM.cs
  10. 27
      src/Squidex/Controllers/UI/Extensions.cs
  11. 2
      src/Squidex/Controllers/UI/ExternalProvider.cs
  12. 58
      src/Squidex/Controllers/UI/Profile/ProfileController.cs
  13. 7
      src/Squidex/Controllers/UI/Profile/ProfileVM.cs
  14. 21
      src/Squidex/Controllers/UI/Profile/RemoveLoginModel.cs
  15. 13
      src/Squidex/Views/Account/AccessDenied.cshtml
  16. 2
      src/Squidex/Views/Account/LockedOut.cshtml
  17. 60
      src/Squidex/Views/Profile/Profile.cshtml
  18. 33
      src/Squidex/app/theme/_bootstrap.scss
  19. 7
      src/Squidex/app/theme/_static.scss
  20. 4
      src/Squidex/app/theme/_vars.scss

1
src/Squidex.Infrastructure/Assets/FolderAssetStore.cs

@ -6,7 +6,6 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;

5
src/Squidex.Read.MongoDb/Users/WrappedIdentityUser.cs

@ -27,6 +27,11 @@ namespace Squidex.Read.MongoDb.Users
get { return Claims.Select(x => new Claim(x.Type, x.Value)).ToList(); } get { return Claims.Select(x => new Claim(x.Type, x.Value)).ToList(); }
} }
IReadOnlyList<ExternalLogin> IUser.Logins
{
get { return Logins.Select(x => new ExternalLogin(x.LoginProvider, x.ProviderKey, x.ProviderDisplayName)).ToList(); }
}
public void UpdateEmail(string email) public void UpdateEmail(string email)
{ {
Email = UserName = email; Email = UserName = email;

32
src/Squidex.Read/Users/ExternalLogin.cs

@ -0,0 +1,32 @@
// ==========================================================================
// ExternalLogin.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Read.Users
{
public sealed class ExternalLogin
{
public string LoginProvider { get; }
public string ProviderKey { get; }
public string ProviderDisplayName { get; }
public ExternalLogin(string loginProvider, string providerKey, string providerDisplayName)
{
LoginProvider = loginProvider;
ProviderKey = providerKey;
ProviderDisplayName = providerDisplayName;
if (string.IsNullOrWhiteSpace(ProviderDisplayName))
{
ProviderDisplayName = loginProvider;
}
}
}
}

2
src/Squidex.Read/Users/IUser.cs

@ -23,6 +23,8 @@ namespace Squidex.Read.Users
IReadOnlyList<Claim> Claims { get; } IReadOnlyList<Claim> Claims { get; }
IReadOnlyList<ExternalLogin> Logins { get; }
void UpdateEmail(string email); void UpdateEmail(string email);
void AddClaim(Claim claim); void AddClaim(Claim claim);

2
src/Squidex.Read/Users/UserExtensions.cs

@ -17,7 +17,7 @@ namespace Squidex.Read.Users
{ {
public static class UserExtensions public static class UserExtensions
{ {
public static void SetDisplayName(this IUser user, string displayName) public static void UpdateDisplayName(this IUser user, string displayName)
{ {
user.SetClaim(SquidexClaimTypes.SquidexDisplayName, displayName); user.SetClaim(SquidexClaimTypes.SquidexDisplayName, displayName);
} }

4
src/Squidex.Read/Users/UserManagerExtensions.cs

@ -53,7 +53,7 @@ namespace Squidex.Read.Users
{ {
var user = factory.Create(email); var user = factory.Create(email);
user.SetDisplayName(displayName); user.UpdateDisplayName(displayName);
user.SetPictureUrlFromGravatar(email); user.SetPictureUrlFromGravatar(email);
await DoChecked(() => userManager.CreateAsync(user), "Cannot create user."); await DoChecked(() => userManager.CreateAsync(user), "Cannot create user.");
@ -82,7 +82,7 @@ namespace Squidex.Read.Users
if (!string.IsNullOrWhiteSpace(displayName)) if (!string.IsNullOrWhiteSpace(displayName))
{ {
user.SetDisplayName(displayName); user.UpdateDisplayName(displayName);
} }
await DoChecked(() => userManager.UpdateAsync(user), "Cannot update user."); await DoChecked(() => userManager.UpdateAsync(user), "Cannot update user.");

1
src/Squidex/Controllers/Api/Users/UsersController.cs

@ -16,7 +16,6 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations; using NSwag.Annotations;
using Squidex.Controllers.Api.Users.Models; using Squidex.Controllers.Api.Users.Models;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Read.Users; using Squidex.Read.Users;

12
src/Squidex/Controllers/UI/Account/AccountController.cs

@ -44,7 +44,7 @@ namespace Squidex.Controllers.UI.Account
private readonly IIdentityServerInteractionService interactions; private readonly IIdentityServerInteractionService interactions;
public AccountController( public AccountController(
SignInManager<IUser> signInManager, SignInManager<IUser> signInManager,
UserManager<IUser> userManager, UserManager<IUser> userManager,
IUserFactory userFactory, IUserFactory userFactory,
IOptions<MyIdentityOptions> identityOptions, IOptions<MyIdentityOptions> identityOptions,
@ -86,7 +86,7 @@ namespace Squidex.Controllers.UI.Account
[Route("account/accessdenied")] [Route("account/accessdenied")]
public IActionResult AccessDenied() public IActionResult AccessDenied()
{ {
return View("LockedOut"); return View("AccessDenied");
} }
[HttpGet] [HttpGet]
@ -189,16 +189,16 @@ namespace Squidex.Controllers.UI.Account
{ {
var properties = var properties =
signInManager.ConfigureExternalAuthenticationProperties(provider, signInManager.ConfigureExternalAuthenticationProperties(provider,
Url.Action(nameof(Callback), new { ReturnUrl = returnUrl })); Url.Action(nameof(ExternalCallback), new { ReturnUrl = returnUrl }));
return Challenge(properties, provider); return Challenge(properties, provider);
} }
[HttpGet] [HttpGet]
[Route("account/callback/")] [Route("account/external-callback/")]
public async Task<IActionResult> Callback(string returnUrl = null, string remoteError = null) public async Task<IActionResult> ExternalCallback(string returnUrl = null, string remoteError = null)
{ {
var externalLogin = await signInManager.GetExternalLoginInfoAsync(); var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync();
if (externalLogin == null) if (externalLogin == null)
{ {

2
src/Squidex/Controllers/UI/Account/LoginVM.cs

@ -22,6 +22,6 @@ namespace Squidex.Controllers.UI.Account
public bool HasPasswordAndExternal { get; set; } public bool HasPasswordAndExternal { get; set; }
public IEnumerable<ExternalProvider> ExternalProviders { get; set; } public IReadOnlyList<ExternalProvider> ExternalProviders { get; set; }
} }
} }

27
src/Squidex/Controllers/UI/Extensions.cs

@ -0,0 +1,27 @@
// ==========================================================================
// Extensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Squidex.Read.Users;
namespace Squidex.Controllers.UI
{
public static class Extensions
{
public static async Task<ExternalLoginInfo> GetExternalLoginInfoWithDisplayNameAsync(this SignInManager<IUser> signInManager, string expectedXsrf = null)
{
var externalLogin = await signInManager.GetExternalLoginInfoAsync(expectedXsrf);
externalLogin.ProviderDisplayName = externalLogin.Principal.FindFirst(ClaimTypes.Email).Value;
return externalLogin;
}
}
}

2
src/Squidex/Controllers/UI/Account/ExternalProvider.cs → src/Squidex/Controllers/UI/ExternalProvider.cs

@ -6,7 +6,7 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
namespace Squidex.Controllers.UI.Account namespace Squidex.Controllers.UI
{ {
public class ExternalProvider public class ExternalProvider
{ {

58
src/Squidex/Controllers/UI/Profile/ProfileController.cs

@ -17,7 +17,6 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using NSwag.Annotations; using NSwag.Annotations;
using Squidex.Config;
using Squidex.Config.Identity; using Squidex.Config.Identity;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
@ -29,18 +28,24 @@ namespace Squidex.Controllers.UI.Profile
[SwaggerIgnore] [SwaggerIgnore]
public class ProfileController : Controller public class ProfileController : Controller
{ {
private readonly SignInManager<IUser> signInManager;
private readonly UserManager<IUser> userManager; private readonly UserManager<IUser> userManager;
private readonly IUserPictureStore userPictureStore; private readonly IUserPictureStore userPictureStore;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly IOptions<MyIdentityOptions> identityOptions; private readonly IOptions<MyIdentityOptions> identityOptions;
private readonly IOptions<IdentityCookieOptions> identityCookieOptions;
public ProfileController( public ProfileController(
SignInManager<IUser> signInManager,
UserManager<IUser> userManager, UserManager<IUser> userManager,
IUserPictureStore userPictureStore, IUserPictureStore userPictureStore,
IAssetThumbnailGenerator assetThumbnailGenerator, IAssetThumbnailGenerator assetThumbnailGenerator,
IOptions<MyIdentityOptions> identityOptions) IOptions<MyIdentityOptions> identityOptions,
IOptions<IdentityCookieOptions> identityCookieOptions)
{ {
this.signInManager = signInManager;
this.identityOptions = identityOptions; this.identityOptions = identityOptions;
this.identityCookieOptions = identityCookieOptions;
this.userManager = userManager; this.userManager = userManager;
this.userPictureStore = userPictureStore; this.userPictureStore = userPictureStore;
this.assetThumbnailGenerator = assetThumbnailGenerator; this.assetThumbnailGenerator = assetThumbnailGenerator;
@ -64,14 +69,39 @@ namespace Squidex.Controllers.UI.Profile
return MakeChangeAsync(async user => return MakeChangeAsync(async user =>
{ {
user.UpdateEmail(model.Email); user.UpdateEmail(model.Email);
user.SetDisplayName(model.DisplayName); user.UpdateDisplayName(model.DisplayName);
return await userManager.UpdateAsync(user); return await userManager.UpdateAsync(user);
}, "Account updated successfully. Please logout and login again to see the changes."); }, "Account updated successfully. Please logout and login again to see the changes.");
} }
[HttpPost] [HttpPost]
[Route("/account/setpassword")] [Route("account/add-login/")]
public async Task<IActionResult> AddLogin(string provider)
{
await HttpContext.Authentication.SignOutAsync(identityCookieOptions.Value.ExternalCookieAuthenticationScheme);
var properties =
signInManager.ConfigureExternalAuthenticationProperties(provider,
Url.Action(nameof(AddLoginCallback)), userManager.GetUserId(User));
return Challenge(properties, provider);
}
[HttpGet]
[Route("account/add-login-callback/")]
public Task<IActionResult> AddLoginCallback(string remoteError = null)
{
return MakeChangeAsync(async user =>
{
var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync(userManager.GetUserId(User));
return await userManager.AddLoginAsync(user, externalLogin);
}, "Login added successfully.");
}
[HttpPost]
[Route("/account/set-password")]
public Task<IActionResult> SetPassword(SetPasswordModel model) public Task<IActionResult> SetPassword(SetPasswordModel model)
{ {
return MakeChangeAsync(user => userManager.AddPasswordAsync(user, model.Password), return MakeChangeAsync(user => userManager.AddPasswordAsync(user, model.Password),
@ -79,7 +109,7 @@ namespace Squidex.Controllers.UI.Profile
} }
[HttpPost] [HttpPost]
[Route("/account/changepassword")] [Route("/account/change-password")]
public Task<IActionResult> ChangePassword(ChangePasswordModel model) public Task<IActionResult> ChangePassword(ChangePasswordModel model)
{ {
return MakeChangeAsync(user => userManager.ChangePasswordAsync(user, model.OldPassword, model.Password), return MakeChangeAsync(user => userManager.ChangePasswordAsync(user, model.OldPassword, model.Password),
@ -87,7 +117,15 @@ namespace Squidex.Controllers.UI.Profile
} }
[HttpPost] [HttpPost]
[Route("/account/picture")] [Route("/account/remove-login")]
public Task<IActionResult> RemoveLogin(RemoveLoginModel model)
{
return MakeChangeAsync(user => userManager.RemoveLoginAsync(user, model.LoginProvider, model.ProviderKey),
"Login provider removed successfully.");
}
[HttpPost]
[Route("/account/upload-picture")]
public Task<IActionResult> UploadPicture(List<IFormFile> file) public Task<IActionResult> UploadPicture(List<IFormFile> file)
{ {
return MakeChangeAsync(async user => return MakeChangeAsync(async user =>
@ -132,6 +170,8 @@ namespace Squidex.Controllers.UI.Profile
if (result.Succeeded) if (result.Succeeded)
{ {
await signInManager.SignInAsync(user, true);
return RedirectToAction(nameof(Profile), new { successMessage }); return RedirectToAction(nameof(Profile), new { successMessage });
} }
@ -147,10 +187,16 @@ namespace Squidex.Controllers.UI.Profile
private async Task<ProfileVM> GetProfileVM(IUser user, ChangeProfileModel model = null) private async Task<ProfileVM> GetProfileVM(IUser user, ChangeProfileModel model = null)
{ {
var providers =
signInManager.GetExternalAuthenticationSchemes()
.Select(x => new ExternalProvider(x.AuthenticationScheme, x.DisplayName)).ToList();
var result = new ProfileVM var result = new ProfileVM
{ {
Id = user.Id, Id = user.Id,
Email = user.Email, Email = user.Email,
ExternalLogins = user.Logins,
ExternalProviders = providers,
DisplayName = user.DisplayName(), DisplayName = user.DisplayName(),
HasPassword = await userManager.HasPasswordAsync(user), HasPassword = await userManager.HasPasswordAsync(user),
HasPasswordAuth = identityOptions.Value.AllowPasswordAuth HasPasswordAuth = identityOptions.Value.AllowPasswordAuth

7
src/Squidex/Controllers/UI/Profile/ProfileVM.cs

@ -6,6 +6,9 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using Squidex.Read.Users;
namespace Squidex.Controllers.UI.Profile namespace Squidex.Controllers.UI.Profile
{ {
public sealed class ProfileVM public sealed class ProfileVM
@ -19,5 +22,9 @@ namespace Squidex.Controllers.UI.Profile
public bool HasPassword { get; set; } public bool HasPassword { get; set; }
public bool HasPasswordAuth { get; set; } public bool HasPasswordAuth { get; set; }
public IReadOnlyList<ExternalLogin> ExternalLogins { get; set; }
public IReadOnlyList<ExternalProvider> ExternalProviders { get; set; }
} }
} }

21
src/Squidex/Controllers/UI/Profile/RemoveLoginModel.cs

@ -0,0 +1,21 @@
// ==========================================================================
// RemoveLoginModel.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.UI.Profile
{
public class RemoveLoginModel
{
[Required(ErrorMessage = "Login provider is required.")]
public string LoginProvider { get; set; }
[Required(ErrorMessage = "Provider key.")]
public string ProviderKey { get; set; }
}
}

13
src/Squidex/Views/Account/AccessDenied.cshtml

@ -0,0 +1,13 @@
@{
ViewBag.Title = "Account locked";
}
<h1 class="splash-h1">Access denied</h1>
<p class="splash-text">
This operation is not allowed.
</p>
<p class="splash-text">
<a href="~/identity-server/account/logout-redirect">Logout</a>
</p>

2
src/Squidex/Views/Account/LockedOut.cshtml

@ -9,5 +9,5 @@
</p> </p>
<p class="splash-text"> <p class="splash-text">
<a href="/identity-server/account/logout-redirect">Logout</a> <a href="~/identity-server/account/logout-redirect">Logout</a>
</p> </p>

60
src/Squidex/Views/Profile/Profile.cshtml

@ -17,7 +17,7 @@
@if (!string.IsNullOrWhiteSpace(ViewBag.SuccessMessage)) @if (!string.IsNullOrWhiteSpace(ViewBag.SuccessMessage))
{ {
<div class="form-alert form-alert-success"> <div class="form-alert form-alert-success" id="success">
@ViewBag.SuccessMessage @ViewBag.SuccessMessage
</div> </div>
} }
@ -74,6 +74,53 @@
<button type="submit" class="btn btn-primary">Save</button> <button type="submit" class="btn btn-primary">Save</button>
</form> </form>
<div class="profile-section">
<h2>Logins</h2>
<table class="table table-fixed table-lesspadding">
<colgroup>
<col style="width: 100px;" />
<col style="width: 100%;" />
<col style="width: 100px;" />
</colgroup>
@foreach (var login in Model.ExternalLogins)
{
<tr>
<td>
<span>@login.LoginProvider</span>
</td>
<td>
<span class="truncate">@login.ProviderDisplayName</span>
</td>
<td class="text-right">
@if (Model.ExternalLogins.Count > 1 || Model.HasPassword)
{
<form asp-controller="Profile" asp-action="RemoveLogin" method="post">
<input type="hidden" value="@login.LoginProvider" name="LoginProvider" />
<input type="hidden" value="@login.ProviderKey" name="ProviderKey" />
<button type="submit" class="btn btn-link btn-danger btn-sm">
Remove
</button>
</form>
}
</td>
</tr>
}
</table>
<form asp-controller="Profile" asp-action="AddLogin" method="post">
@foreach (var provider in Model.ExternalProviders)
{
var schema = provider.AuthenticationScheme.ToLowerInvariant();
<button class="btn external-button-small btn btn-@schema" type="submit" name="provider" value="@provider.AuthenticationScheme">
<i class="icon-@schema external-icon"></i>
</button>
}
</form>
</div>
<div class="profile-section"> <div class="profile-section">
@if (Model.HasPasswordAuth) @if (Model.HasPasswordAuth)
{ {
@ -175,7 +222,16 @@
}); });
pictureInput.addEventListener('change', pictureInput.addEventListener('change',
function () { function() {
pictureForm.submit(); pictureForm.submit();
}); });
var successMessage = document.getElementById('success');
if (successMessage) {
setTimeout(function() {
successMessage.remove();
},
5000);
}
</script> </script>

33
src/Squidex/app/theme/_bootstrap.scss

@ -175,14 +175,35 @@ h3 {
// Buttons for external logins. // Buttons for external logins.
&-github { &-github {
@include button-variant($color-dark-foreground, $color-extern-github, $color-extern-github); @include button-variant($color-dark-foreground, $color-extern-github, $color-extern-github);
&:hover,
&:focus {
.icon-github {
color: darken($color-extern-github-icon, 5%);
}
}
} }
&-google { &-google {
@include button-variant($color-dark-foreground, $color-extern-google, $color-extern-google); @include button-variant($color-dark-foreground, $color-extern-google, $color-extern-google);
&:hover,
&:focus {
.icon-google {
color: darken($color-extern-google-icon, 5%);
}
}
} }
&-microsoft { &-microsoft {
@include button-variant($color-dark-foreground, $color-extern-microsoft, $color-extern-microsoft); @include button-variant($color-dark-foreground, $color-extern-microsoft, $color-extern-microsoft);
&:hover,
&:focus {
.icon-microsoft {
color: darken($color-extern-microsoft-icon, 5%);
}
}
} }
// Special radio button. // Special radio button.
@ -326,4 +347,16 @@ h3 {
border: 0; border: 0;
} }
} }
&-lesspadding {
td {
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
}
}
}
} }

7
src/Squidex/app/theme/_static.scss

@ -110,6 +110,13 @@ noscript {
line-height: 1.8rem; line-height: 1.8rem;
} }
&-button-small {
text-align: center;
padding-left: 0;
padding-right: 0;
width: 40px;
}
&-icon { &-icon {
display: inline-block; display: inline-block;
font-size: 1.3rem; font-size: 1.3rem;

4
src/Squidex/app/theme/_vars.scss

@ -12,8 +12,8 @@ $color-control: rgba(0, 0, 0, .15);
$color-input: #dbe4eb; $color-input: #dbe4eb;
$color-disabled: #eef1f4; $color-disabled: #eef1f4;
$color-extern-google: #b02c1b; $color-extern-google: #d34836;
$color-extern-google-icon: #d34836; $color-extern-google-icon: #b02c1b;
$color-extern-microsoft: #004185; $color-extern-microsoft: #004185;
$color-extern-microsoft-icon: #1b67b7; $color-extern-microsoft-icon: #1b67b7;
$color-extern-github: #191919; $color-extern-github: #191919;

Loading…
Cancel
Save