Skip to content

Commit

Permalink
#218 - Client-side profile change.
Browse files Browse the repository at this point in the history
  • Loading branch information
maraf committed Feb 22, 2019
1 parent 5a7fb7f commit b107a4a
Show file tree
Hide file tree
Showing 18 changed files with 364 additions and 44 deletions.
4 changes: 3 additions & 1 deletion src/Money.UI.Backend/Bootstrap/BootstrapTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,9 @@ private void Domain()

bootstrapTask.Initialize();

commandDispatcher.Handlers.AddAll(new UserHandler(services.BuildServiceProvider().GetRequiredService<UserManager<ApplicationUser>>(), eventDispatcher));
UserHandler userHandler = new UserHandler(services.BuildServiceProvider().GetRequiredService<UserManager<ApplicationUser>>(), eventDispatcher);
commandDispatcher.Handlers.AddAll(userHandler);
queryDispatcher.AddAll(userHandler);
}

private void ReadModels()
Expand Down
76 changes: 60 additions & 16 deletions src/Money.UI.Backend/Commands/Handlers/UserHandler.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
using Microsoft.AspNetCore.Identity;
using Money.Events;
using Money.Models;
using Money.Queries;
using Neptuo;
using Neptuo.Collections.Specialized;
using Neptuo.Commands;
using Neptuo.Commands.Handlers;
using Neptuo.Events;
using Neptuo.Models;
using Neptuo.Models.Keys;
using Neptuo.Queries.Handlers;
using System;
using System.Collections.Generic;
using System.Linq;
Expand All @@ -15,7 +18,7 @@

namespace Money.Commands.Handlers
{
public class UserHandler : ICommandHandler<Envelope<ChangePassword>>
public class UserHandler : ICommandHandler<Envelope<ChangePassword>>, ICommandHandler<Envelope<ChangeEmail>>, IQueryHandler<GetProfile, ProfileModel>
{
private readonly UserManager<ApplicationUser> userManager;
private readonly IEventDispatcher eventDispatcher;
Expand All @@ -28,25 +31,66 @@ public UserHandler(UserManager<ApplicationUser> userManager, IEventDispatcher ev
this.eventDispatcher = eventDispatcher;
}

public async Task HandleAsync(Envelope<ChangePassword> command)
private T DecorateException<T>(ICommand command, StringKey userKey, T ex)
where T : AggregateRootException
{
AggregateRootExceptionDecorator decorator = new AggregateRootExceptionDecorator(ex);
decorator.SetCommandKey(command.Key);
decorator.SetSourceCommandKey(command.Key);
decorator.SetKey(userKey);
return ex;
}

private async Task<(ApplicationUser Model, StringKey Key)> GetUserAsync(Envelope envelope)
{
StringKey userKey = envelope.Metadata.Get<StringKey>("UserKey");
return (await GetUserAsync(userKey), userKey);
}

private async Task<ApplicationUser> GetUserAsync(StringKey userKey)
{
StringKey userKey = command.Metadata.Get<StringKey>("UserKey");
ApplicationUser user = await userManager.FindByIdAsync(userKey.Identifier);
if (user == null)
ApplicationUser model = await userManager.FindByIdAsync(userKey.Identifier);
if (model == null)
throw new InvalidOperationException($"Unable to load user with ID '{userKey.Identifier}'.");

IdentityResult result = await userManager.ChangePasswordAsync(user, command.Body.Current, command.Body.New);
return model;
}

private void EnsureNotDemo<T>(Envelope<T> envelope, (ApplicationUser Model, StringKey Key) user)
where T : ICommand
{
if (user.Model.IsDemo())
throw DecorateException(envelope.Body, user.Key, new DemoUserCantBeChangedException());
}

public async Task HandleAsync(Envelope<ChangePassword> command)
{
var user = await GetUserAsync(command);
EnsureNotDemo(command, user);

var result = await userManager.ChangePasswordAsync(user.Model, command.Body.Current, command.Body.New);
if (!result.Succeeded)
throw DecorateException(command.Body, user.Key, new PasswordChangeFailedException(String.Join(Environment.NewLine, result.Errors.Select(e => e.Description))));

await eventDispatcher.PublishAsync(new PasswordChanged(GuidKey.Create(Guid.NewGuid(), "PasswordChanged"), user.Key));
}

public async Task HandleAsync(Envelope<ChangeEmail> command)
{
var user = await GetUserAsync(command);
EnsureNotDemo(command, user);

var result = await userManager.SetEmailAsync(user.Model, command.Body.Email);
if (!result.Succeeded)
{
var ex = new PasswordChangeFailedException(String.Join(Environment.NewLine, result.Errors.Select(e => e.Description)));
AggregateRootExceptionDecorator decorator = new AggregateRootExceptionDecorator(ex);
decorator.SetCommandKey(command.Body.Key);
decorator.SetSourceCommandKey(command.Body.Key);
decorator.SetKey(userKey);
throw ex;
}

await eventDispatcher.PublishAsync(new PasswordChanged(GuidKey.Create(Guid.NewGuid(), "PasswordChanged"), userKey));
throw DecorateException(command.Body, user.Key, new EmailChangeFailedException());

await eventDispatcher.PublishAsync(new EmailChanged(GuidKey.Create(Guid.NewGuid(), "EmailChanged"), user.Key, command.Body.Email));
}

public async Task<ProfileModel> HandleAsync(GetProfile query)
{
var user = await GetUserAsync(query.UserKey.AsStringKey());
return new ProfileModel(user.UserName, user.Email);
}
}
}
3 changes: 2 additions & 1 deletion src/Money.UI.Backend/Hubs/ApiHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class ApiHub : Hub,
IEventHandler<CurrencyCreated>, IEventHandler<CurrencyDeleted>, IEventHandler<CurrencyDefaultChanged>, IEventHandler<CurrencySymbolChanged>,
IEventHandler<CurrencyExchangeRateSet>, IEventHandler<CurrencyExchangeRateRemoved>,
IEventHandler<OutcomeCreated>, IEventHandler<OutcomeDeleted>, IEventHandler<OutcomeAmountChanged>, IEventHandler<OutcomeDescriptionChanged>, IEventHandler<OutcomeWhenChanged>,
IEventHandler<PasswordChanged>
IEventHandler<PasswordChanged>, IEventHandler<EmailChanged>
{
private readonly FormatterContainer formatters;

Expand Down Expand Up @@ -128,6 +128,7 @@ private Task RaiseEvent<T>(T payload)
Task IEventHandler<OutcomeWhenChanged>.HandleAsync(OutcomeWhenChanged payload) => RaiseEvent(payload);

Task IEventHandler<PasswordChanged>.HandleAsync(PasswordChanged payload) => RaiseEvent(payload);
Task IEventHandler<EmailChanged>.HandleAsync(EmailChanged payload) => RaiseEvent(payload);

public void Handle(AggregateRootException exception)
{
Expand Down
1 change: 1 addition & 0 deletions src/Money.UI.Backend/Models/ClaimsPrincipalExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ public static class ClaimsPrincipalExtensions
public const string DemoUserPassword = "demo";

public static bool IsDemo(this ClaimsPrincipal user) => user.Identity.Name == DemoUserName;
public static bool IsDemo(this ApplicationUser user) => user.UserName == DemoUserName;
}
}
20 changes: 10 additions & 10 deletions src/Money.UI.Blazor/Bootstrap/BootstrapTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,16 +116,16 @@ private void Domain()

private void QueryMiddlewares(IEventHandlerCollection handlers)
{
CategoryMiddleware categoryMiddleware = new CategoryMiddleware();
CurrencyMiddleware currencyMiddleware = new CurrencyMiddleware();

handlers
.AddAll(categoryMiddleware)
.AddAll(currencyMiddleware);

services
.AddSingleton<HttpQueryDispatcher.IMiddleware>(categoryMiddleware)
.AddSingleton<HttpQueryDispatcher.IMiddleware>(currencyMiddleware);
void AddMiddlewareAndEventHandler<T>(T handler)
where T : HttpQueryDispatcher.IMiddleware
{
handlers.AddAll(handler);
services.AddSingleton<HttpQueryDispatcher.IMiddleware>(handler);
}

AddMiddlewareAndEventHandler(new CategoryMiddleware());
AddMiddlewareAndEventHandler(new CurrencyMiddleware());
AddMiddlewareAndEventHandler(new UserMiddleware());
}
}
}
31 changes: 31 additions & 0 deletions src/Money.UI.Blazor/Commands/ChangeEmail.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Neptuo;
using Neptuo.Commands;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Money.Commands
{
/// <summary>
/// A command for changing user's email.
/// </summary>
public class ChangeEmail : Command
{
/// <summary>
/// Gets an user's email.
/// </summary>
public string Email { get; }

/// <summary>
/// Creates a new instance.
/// </summary>
/// <param name="email">An user's email.</param>
public ChangeEmail(string email)
{
Ensure.NotNull(email, "email");
Email = email;
}
}
}
15 changes: 15 additions & 0 deletions src/Money.UI.Blazor/Commands/DemoUserCantBeChangedException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Neptuo.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Money.Commands
{
/// <summary>
/// An excception raised when demo user tries to change it's account.
/// </summary>
public class DemoUserCantBeChangedException : AggregateRootException
{ }
}
15 changes: 15 additions & 0 deletions src/Money.UI.Blazor/Commands/EmailChangeFailedException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Neptuo.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Money.Commands
{
/// <summary>
/// An exception raised when user email change fails.
/// </summary>
public class EmailChangeFailedException : AggregateRootException
{ }
}
4 changes: 4 additions & 0 deletions src/Money.UI.Blazor/Components/ExceptionPanel.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,12 @@
message = MessageBuilder.CantDeleteDefaultCurrency();
else if (e is CantDeleteLastCurrencyException)
message = MessageBuilder.CantDeleteLastCurrency();
else if (e is DemoUserCantBeChangedException)
message = MessageBuilder.DemoUserCantBeChanged();
else if (e is PasswordChangeFailedException passwordChangeFailed)
message = MessageBuilder.PasswordChangeFailed(passwordChangeFailed.ErrorDescription);
else if (e is EmailChangeFailedException)
message = MessageBuilder.EmailChangeFailed();

Message = message;
}
Expand Down
24 changes: 24 additions & 0 deletions src/Money.UI.Blazor/Events/EmailChanged.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Neptuo.Models.Keys;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Money.Events
{
/// <summary>
/// An event raised when user email has been changed.
/// </summary>
public class EmailChanged : UserEvent
{
public string Email { get; }

public EmailChanged(IKey key, IKey aggregateKey, string email)
: base(key, aggregateKey, 0)
{
UserKey = aggregateKey;
Email = email;
}
}
}
1 change: 1 addition & 0 deletions src/Money.UI.Blazor/Models/Api/CommandMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public CommandMapper()
Add<DeleteCurrency>("currency-delete");

Add<ChangePassword>("user-change-password");
Add<ChangeEmail>("user-change-email");
}
}
}
3 changes: 3 additions & 0 deletions src/Money.UI.Blazor/Models/Api/QueryMapper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Money.Models.Queries;
using Money.Queries;
using System;
using System.Collections.Generic;
using System.Linq;
Expand Down Expand Up @@ -30,6 +31,8 @@ public QueryMapper()
Add<ListYearOutcomeFromCategory>("year-outcome-from-category");

Add<SearchOutcomes>("search-outcomes");

Add<GetProfile>("user-profile");
}
}
}
37 changes: 37 additions & 0 deletions src/Money.UI.Blazor/Models/User/ProfileModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Neptuo;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Money.Models
{
/// <summary>
/// An user profile information.
/// </summary>
public class ProfileModel
{
/// <summary>
/// Gets an username.
/// </summary>
public string UserName { get; set; }

/// <summary>
/// Gets an user's email.
/// </summary>
public string Email { get; set; }

/// <summary>
/// Creates a new instance.
/// </summary>
/// <param name="userName">An username.</param>
/// <param name="email">An user's email.</param>
public ProfileModel(string userName, string email)
{
Ensure.NotNull(userName, "userName");
UserName = userName;
Email = email;
}
}
}
28 changes: 12 additions & 16 deletions src/Money.UI.Blazor/Pages/User/Profile.cshtml
Original file line number Diff line number Diff line change
@@ -1,33 +1,29 @@
@page "/user"
@inherits ProfileBase

<UserHead />

<div class="user">
<ExceptionPanel />

@if (IsSuccess)
{
<Alert Message="Your email has been changed." Mode="AlertMode.Success" IsDismissible="true" />
}

<div class="row">
<div class="col-md-6">
<form method="post" onsubmit="OnFormSubmit">
<form method="post" onsubmit="@OnFormSubmit">
<div class="form-group">
<label for="UserName">UserName</label>
<input class="form-control" disabled="" type="text" id="UserName" name="UserName" value="maraf">
<input class="form-control" disabled type="text" id="UserName" value="@UserName" />
</div>
<div class="form-group">
<label for="Email">Email</label>
<div class="input-group">
<input class="form-control" autofocus type="email" id="Email" name="Email" value="">
<span class="input-group-addon" aria-hidden="true"><span class="glyphicon glyphicon-ok text-success"></span></span>
</div>
<span class="text-danger field-validation-valid" data-valmsg-for="Email" data-valmsg-replace="true"></span>
<input class="form-control" autofocus type="email" id="Email" bind="@Email" />
</div>
<button type="submit" class="btn btn-default">Save</button>
</form>
</div>
</div>
</div>

@functions
{
private async Task OnFormSubmit()
{

}
}
</div>
Loading

0 comments on commit b107a4a

Please sign in to comment.