Skip to content

Commit

Permalink
MAJOR REVAMP TO ACCOUNT/POST-REDIRECT-GET:
Browse files Browse the repository at this point in the history
-Added missing FluentValidation initializer in StartupConfig.cs
- Fixed bug with Account System where ViewModel wasn't passed in Post-Redirect-Get, so added to IModelStateService calls to Store, Get, and Clear (when redirecting off) the ViewModel, and updated the Account features to leverage this.
-Fixed regex issue on Password Policy
-Removed "Public" default user logic on UserRepository.GetUser(user identity).  Still returns Public on GetCurrentUser() though if no logged in user found.
  • Loading branch information
KenticoDevTrev committed Jan 19, 2022
1 parent 973f2a3 commit 14189cb
Show file tree
Hide file tree
Showing 39 changed files with 277 additions and 96 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ public async Task<User> GetUserAsync(int userID)
cs.CacheDependency = builder.GetCMSCacheDependency();
}
var user = await _userInfoProvider.GetAsync(userID);
user ??= await _userInfoProvider.GetAsync("public");
return user;
}, new CacheSettings(15, "GetUserAsync", userID));
return user != null ? _mapper.Map<User>(user) : null;
Expand All @@ -73,7 +72,6 @@ public async Task<User> GetUserAsync(string userName)
cs.CacheDependency = builder.GetCMSCacheDependency();
}
var user = await _userInfoProvider.GetAsync(userName);
user ??= await _userInfoProvider.GetAsync("public");
return user;
}, new CacheSettings(15, "GetUserAsync", userName));
return user != null ? _mapper.Map<User>(user) : null;
Expand All @@ -91,7 +89,6 @@ public async Task<User> GetUserAsync(Guid userGuid)
cs.CacheDependency = builder.GetCMSCacheDependency();
}
var user = await _userInfoProvider.GetAsync(userGuid);
user ??= await _userInfoProvider.GetAsync("public");
return user;
}, new CacheSettings(15, "GetUserAsync", userGuid));
return user != null ? _mapper.Map<User>(user) : null;
Expand All @@ -112,7 +109,6 @@ public async Task<User> GetUserByEmailAsync(string email)
.WhereEquals(nameof(UserInfo.Email), email)
.TopN(1)
.GetEnumerableTypedResultAsync()).FirstOrDefault();
user ??= await _userInfoProvider.GetAsync("public");
return user;
}, new CacheSettings(15, "GetUserByEmailAsync", email));
return user != null ? _mapper.Map<User>(user) : null;
Expand Down
38 changes: 33 additions & 5 deletions MVC/MVC.Libraries/Services/Implementations/ModelStateService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,14 @@
using Generic.Services.Interfaces;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Text.Json;

namespace Generic.Services.Implementations
{
public class ModelStateService : IModelStateService
{
public void MergeModelState(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelState, ITempDataDictionary tempData)
public void MergeModelState(ModelStateDictionary modelState, ITempDataDictionary tempData)
{
string key = typeof(ModelStateTransfer).FullName;
var serialisedModelState = tempData[key] as string;
Expand Down Expand Up @@ -41,5 +38,36 @@ public void MergeModelState(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDict
}
}
}

public void StoreViewModel<TModel>(ITempDataDictionary tempData, TModel viewModel)
{
tempData.Put<TModel>($"GetViewModel_{typeof(TModel).FullName}", viewModel);
}

public TModel GetViewModel<TModel>(ITempDataDictionary tempData)
{
var obj = tempData.Get<TModel>($"GetViewModel_{typeof(TModel).FullName}");
return (obj != null ? (TModel) obj : default(TModel));
}

public void ClearViewModel<TModel>(ITempDataDictionary tempData)
{
tempData.Remove($"GetViewModel_{typeof(TModel).FullName}");
}
}

public static class TempDataExtensions
{
public static void Put<T>(this ITempDataDictionary tempData, string key, T value)
{
tempData[key] = JsonSerializer.Serialize<T>(value);
}

public static T Get<T>(this ITempDataDictionary tempData, string key)
{
object o;
tempData.TryGetValue(key, out o);
return o == null ? default : JsonSerializer.Deserialize<T>((string)o);
}
}
}
40 changes: 40 additions & 0 deletions MVC/MVC.Libraries/Services/Interfaces/IModelStateService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using MVCCaching;
using System;

namespace Generic.Services.Interfaces
{
public interface IModelStateService : IService
{
/// <summary>
/// Merges the Model State (Validation state and errors) from the TempData, this should be called in Post-Redirect-Get methedology so the model state can persist between the POST action and the redirected GET.
/// </summary>
/// <param name="modelState"></param>
/// <param name="tempData"></param>
void MergeModelState(ModelStateDictionary modelState, ITempDataDictionary tempData);

/// <summary>
/// Stores the View model in the Temp Data, this is used in Post-Redirect-Get when the controller's POST modifies the view model and then redirects to the original request.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="tempData"></param>
/// <param name="viewModel"></param>
void StoreViewModel<T>(ITempDataDictionary tempData, T viewModel);

/// <summary>
/// Gets the View Model from the TempData, used in Post-Redirect-Get when redirecting back to the original request.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="tempData"></param>
/// <returns></returns>
T GetViewModel<T>(ITempDataDictionary tempData);

/// <summary>
/// Removes the View Model from TempData, this should be called if you are redirecting away from the calling view.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="tempData"></param>
void ClearViewModel<T>(ITempDataDictionary tempData);
}
}

This file was deleted.

4 changes: 4 additions & 0 deletions MVC/MVC/App_Start/StartupConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
using XperienceCommunity.Authorization;
using XperienceCommunity.Localizer;
using XperienceCommunity.PageBuilderUtilities;
using FluentValidation.AspNetCore;

namespace Generic.App_Start
{
Expand Down Expand Up @@ -72,6 +73,9 @@ public static void RegisterInterfaces(IServiceCollection services, IWebHostEnvir

// Kentico authorization
services.AddKenticoAuthorization();

// Fluent Validator
services.AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblies(new Assembly[] { typeof(Startup).Assembly, typeof(Generic.Libraries.AssemblyInfo).Assembly, typeof(Generic.Models.AssemblyInfo).Assembly }));
}

public static void RegisterKenticoServices(IServiceCollection services, IWebHostEnvironment Environment, IConfiguration Configuration)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Generic.Services.Interfaces;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Text.Json;

namespace Generic.Components.ImportModelState
{
Expand All @@ -25,6 +27,7 @@ public ImportModelStateViewComponent(IModelStateService modelStateService)
public IViewComponentResult Invoke()
{
_modelStateService.MergeModelState(ModelState, TempData);

return Content(string.Empty);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,19 @@ public class ForgotPasswordController : Controller
private readonly ISiteSettingsRepository _siteSettingsRepository;
private readonly IUserService _userService;
private readonly IUrlResolver _urlResolver;
private readonly IModelStateService _modelStateService;

public ForgotPasswordController(IUserRepository userRepository,
ISiteSettingsRepository siteSettingsRepository,
IUserService userService,
IUrlResolver urlResolver)
IUrlResolver urlResolver,
IModelStateService modelStateService)
{
_userRepository = userRepository;
_siteSettingsRepository = siteSettingsRepository;
_userService = userService;
_urlResolver = urlResolver;
_modelStateService = modelStateService;
}

/// <summary>
Expand Down Expand Up @@ -68,6 +71,8 @@ public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
model.Error = ex.Message;
}

_modelStateService.StoreViewModel(TempData, model);

return Redirect(forgotPasswordUrl);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@
@section head{
<vc:page-meta-data />
}
<vc:import-model-state />
<vc:forgot-password model=null />
<vc:forgot-password />
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@
@section head{
<vc:page-meta-data />
}
<vc:import-model-state />
<vc:forgot-password model=null />
<vc:forgot-password />
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
using Microsoft.AspNetCore.Mvc;
using Generic.Services.Interfaces;
using Microsoft.AspNetCore.Mvc;

namespace Generic.Features.Account.ForgotPassword
{
[ViewComponent]
public class ForgotPasswordViewComponent : ViewComponent
{
private readonly IModelStateService _modelStateService;

public ForgotPasswordViewComponent(IModelStateService modelStateService)
{
_modelStateService = modelStateService;
}

/// <summary>
/// Uses the current page context to render meta data
/// </summary>
/// <returns></returns>
public IViewComponentResult Invoke(ForgotPasswordViewModel model)
public IViewComponentResult Invoke()
{
model ??= new ForgotPasswordViewModel();
// Hydrate Model State
_modelStateService.MergeModelState(ModelState, TempData);

// Get View Model State
var model = _modelStateService.GetViewModel<ForgotPasswordViewModel>(TempData) ?? new ForgotPasswordViewModel()
{

};

return View("~/Features/Account/ForgotPassword/ForgotPassword.cshtml", model);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.ComponentModel.DataAnnotations;
using System;
using System.ComponentModel.DataAnnotations;

namespace Generic.Features.Account.ForgotPassword
{
[Serializable]
public class ForgotPasswordViewModel
{
[Required]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@model Generic.Features.Account.ForgottenPasswordReset.ForgottenPasswordResetViewModel
@if (Model.Result != null)
@if (Model.Succeeded.HasValue)
{
if (Model.Result.Succeeded)
if (Model.Succeeded.Value)
{
<div class="container">
<h1>Successful</h1>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,19 @@ public class ForgottenPasswordResetController : Controller
private readonly ISiteSettingsRepository _siteSettingsRepository;
private readonly IUserService _userService;
private readonly ILogger _logger;
private readonly IModelStateService _modelStateService;

public ForgottenPasswordResetController(IUserRepository userRepository,
ISiteSettingsRepository siteSettingsRepository,
IUserService userService,
ILogger logger)
ILogger logger,
IModelStateService modelStateService)
{
_userRepository = userRepository;
_siteSettingsRepository = siteSettingsRepository;
_userService = userService;
_logger = logger;
_modelStateService = modelStateService;
}

/// <summary>
Expand Down Expand Up @@ -80,6 +83,11 @@ public async Task<ActionResult> ForgottenPasswordReset(ForgottenPasswordResetVie
model.Result = IdentityResult.Failed(new IdentityError() { Code = "Unknown", Description = "An error occurred." });
_logger.LogException(ex, nameof(ForgottenPasswordResetController), "ForgottenPasswordReset", Description: $"For userid {model.UserID}");
}

// Set this property as the View uses it instead of the IdentityResult, which doesn't serialize/deserialize properly and doesn't make it through the StoreViewModel/GetViewModel
model.Succeeded = model.Result.Succeeded;
_modelStateService.StoreViewModel(TempData, model);

return Redirect(forgottenPasswordResetUrl);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Http;
using Generic.Services.Interfaces;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using System;
Expand All @@ -10,10 +11,13 @@ namespace Generic.Features.Account.ForgottenPasswordReset
public class ForgottenPasswordResetViewComponent : ViewComponent
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IModelStateService _modelStateService;

public ForgottenPasswordResetViewComponent(IHttpContextAccessor httpContextAccessor)
public ForgottenPasswordResetViewComponent(IHttpContextAccessor httpContextAccessor,
IModelStateService modelStateService)
{
_httpContextAccessor = httpContextAccessor;
_modelStateService = modelStateService;
}

/// <summary>
Expand All @@ -22,6 +26,9 @@ public ForgottenPasswordResetViewComponent(IHttpContextAccessor httpContextAcces
/// <returns></returns>
public IViewComponentResult Invoke()
{
// Merge Model State
_modelStateService.MergeModelState(ModelState, TempData);

// Get values from Query String
Guid? userId = null;
string token = null;
Expand All @@ -37,7 +44,7 @@ public IViewComponentResult Invoke()
token = queryToken.FirstOrDefault();
}

var model = new ForgottenPasswordResetViewModel()
var model = _modelStateService.GetViewModel<ForgottenPasswordResetViewModel>(TempData) ?? new ForgottenPasswordResetViewModel()
{
UserID = userId ?? Guid.Empty,
Token = token
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using Microsoft.AspNetCore.Identity;
using FluentValidation;
using Generic.Library.Validation;
using Generic.Repositories.Interfaces;
using Microsoft.AspNetCore.Identity;
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
Expand All @@ -20,11 +23,27 @@ public class ForgottenPasswordResetViewModel

[Required]
[DataType(DataType.Password)]
[Compare(nameof(Password))]
[DisplayName("Confirm New Password")]
public string PasswordConfirm { get; set; }

public IdentityResult Result { get; set; }

/// <summary>
/// Identityresult doesn't serialize/deserialize properly so need to use this as a toggle
/// </summary>
public bool? Succeeded { get; set; }
public string LoginUrl { get; set; }
}

public class ForgottenPasswordResetViewModelValidator : AbstractValidator<ForgottenPasswordResetViewModel>
{
public ForgottenPasswordResetViewModelValidator(ISiteSettingsRepository _siteSettingsRepository)
{
var passwordSettings = _siteSettingsRepository.GetPasswordPolicy();

RuleFor(model => model.Password).ValidPassword(passwordSettings);
RuleFor(model => model.PasswordConfirm).Equal(model => model.Password);

}
}
}
Loading

0 comments on commit 14189cb

Please sign in to comment.