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.
// ==========================================================================
using System;
using System.IO;
using System.Threading.Tasks;
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(); }
}
IReadOnlyList<ExternalLogin> IUser.Logins
{
get { return Logins.Select(x => new ExternalLogin(x.LoginProvider, x.ProviderKey, x.ProviderDisplayName)).ToList(); }
}
public void UpdateEmail(string 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<ExternalLogin> Logins { get; }
void UpdateEmail(string email);
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 void SetDisplayName(this IUser user, string displayName)
public static void UpdateDisplayName(this IUser user, string 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);
user.SetDisplayName(displayName);
user.UpdateDisplayName(displayName);
user.SetPictureUrlFromGravatar(email);
await DoChecked(() => userManager.CreateAsync(user), "Cannot create user.");
@ -82,7 +82,7 @@ namespace Squidex.Read.Users
if (!string.IsNullOrWhiteSpace(displayName))
{
user.SetDisplayName(displayName);
user.UpdateDisplayName(displayName);
}
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 NSwag.Annotations;
using Squidex.Controllers.Api.Users.Models;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
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;
public AccountController(
SignInManager<IUser> signInManager,
SignInManager<IUser> signInManager,
UserManager<IUser> userManager,
IUserFactory userFactory,
IOptions<MyIdentityOptions> identityOptions,
@ -86,7 +86,7 @@ namespace Squidex.Controllers.UI.Account
[Route("account/accessdenied")]
public IActionResult AccessDenied()
{
return View("LockedOut");
return View("AccessDenied");
}
[HttpGet]
@ -189,16 +189,16 @@ namespace Squidex.Controllers.UI.Account
{
var properties =
signInManager.ConfigureExternalAuthenticationProperties(provider,
Url.Action(nameof(Callback), new { ReturnUrl = returnUrl }));
Url.Action(nameof(ExternalCallback), new { ReturnUrl = returnUrl }));
return Challenge(properties, provider);
}
[HttpGet]
[Route("account/callback/")]
public async Task<IActionResult> Callback(string returnUrl = null, string remoteError = null)
[Route("account/external-callback/")]
public async Task<IActionResult> ExternalCallback(string returnUrl = null, string remoteError = null)
{
var externalLogin = await signInManager.GetExternalLoginInfoAsync();
var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync();
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 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.
// ==========================================================================
namespace Squidex.Controllers.UI.Account
namespace Squidex.Controllers.UI
{
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.Extensions.Options;
using NSwag.Annotations;
using Squidex.Config;
using Squidex.Config.Identity;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Reflection;
@ -29,18 +28,24 @@ namespace Squidex.Controllers.UI.Profile
[SwaggerIgnore]
public class ProfileController : Controller
{
private readonly SignInManager<IUser> signInManager;
private readonly UserManager<IUser> userManager;
private readonly IUserPictureStore userPictureStore;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly IOptions<MyIdentityOptions> identityOptions;
private readonly IOptions<IdentityCookieOptions> identityCookieOptions;
public ProfileController(
SignInManager<IUser> signInManager,
UserManager<IUser> userManager,
IUserPictureStore userPictureStore,
IAssetThumbnailGenerator assetThumbnailGenerator,
IOptions<MyIdentityOptions> identityOptions)
IOptions<MyIdentityOptions> identityOptions,
IOptions<IdentityCookieOptions> identityCookieOptions)
{
this.signInManager = signInManager;
this.identityOptions = identityOptions;
this.identityCookieOptions = identityCookieOptions;
this.userManager = userManager;
this.userPictureStore = userPictureStore;
this.assetThumbnailGenerator = assetThumbnailGenerator;
@ -64,14 +69,39 @@ namespace Squidex.Controllers.UI.Profile
return MakeChangeAsync(async user =>
{
user.UpdateEmail(model.Email);
user.SetDisplayName(model.DisplayName);
user.UpdateDisplayName(model.DisplayName);
return await userManager.UpdateAsync(user);
}, "Account updated successfully. Please logout and login again to see the changes.");
}
[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)
{
return MakeChangeAsync(user => userManager.AddPasswordAsync(user, model.Password),
@ -79,7 +109,7 @@ namespace Squidex.Controllers.UI.Profile
}
[HttpPost]
[Route("/account/changepassword")]
[Route("/account/change-password")]
public Task<IActionResult> ChangePassword(ChangePasswordModel model)
{
return MakeChangeAsync(user => userManager.ChangePasswordAsync(user, model.OldPassword, model.Password),
@ -87,7 +117,15 @@ namespace Squidex.Controllers.UI.Profile
}
[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)
{
return MakeChangeAsync(async user =>
@ -132,6 +170,8 @@ namespace Squidex.Controllers.UI.Profile
if (result.Succeeded)
{
await signInManager.SignInAsync(user, true);
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)
{
var providers =
signInManager.GetExternalAuthenticationSchemes()
.Select(x => new ExternalProvider(x.AuthenticationScheme, x.DisplayName)).ToList();
var result = new ProfileVM
{
Id = user.Id,
Email = user.Email,
ExternalLogins = user.Logins,
ExternalProviders = providers,
DisplayName = user.DisplayName(),
HasPassword = await userManager.HasPasswordAsync(user),
HasPasswordAuth = identityOptions.Value.AllowPasswordAuth

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

@ -6,6 +6,9 @@
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Read.Users;
namespace Squidex.Controllers.UI.Profile
{
public sealed class ProfileVM
@ -19,5 +22,9 @@ namespace Squidex.Controllers.UI.Profile
public bool HasPassword { 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 class="splash-text">
<a href="/identity-server/account/logout-redirect">Logout</a>
<a href="~/identity-server/account/logout-redirect">Logout</a>
</p>

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

@ -17,7 +17,7 @@
@if (!string.IsNullOrWhiteSpace(ViewBag.SuccessMessage))
{
<div class="form-alert form-alert-success">
<div class="form-alert form-alert-success" id="success">
@ViewBag.SuccessMessage
</div>
}
@ -74,6 +74,53 @@
<button type="submit" class="btn btn-primary">Save</button>
</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">
@if (Model.HasPasswordAuth)
{
@ -175,7 +222,16 @@
});
pictureInput.addEventListener('change',
function () {
function() {
pictureForm.submit();
});
var successMessage = document.getElementById('success');
if (successMessage) {
setTimeout(function() {
successMessage.remove();
},
5000);
}
</script>

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

@ -175,14 +175,35 @@ h3 {
// Buttons for external logins.
&-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 {
@include button-variant($color-dark-foreground, $color-extern-google, $color-extern-google);
&:hover,
&:focus {
.icon-google {
color: darken($color-extern-google-icon, 5%);
}
}
}
&-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.
@ -326,4 +347,16 @@ h3 {
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;
}
&-button-small {
text-align: center;
padding-left: 0;
padding-right: 0;
width: 40px;
}
&-icon {
display: inline-block;
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-disabled: #eef1f4;
$color-extern-google: #b02c1b;
$color-extern-google-icon: #d34836;
$color-extern-google: #d34836;
$color-extern-google-icon: #b02c1b;
$color-extern-microsoft: #004185;
$color-extern-microsoft-icon: #1b67b7;
$color-extern-github: #191919;

Loading…
Cancel
Save