From 14189cbb4ab7356f0ba734c3286670c4beb8d619 Mon Sep 17 00:00:00 2001 From: Trevor Fayas Date: Wed, 19 Jan 2022 15:31:05 -0800 Subject: [PATCH] MAJOR REVAMP TO ACCOUNT/POST-REDIRECT-GET: -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. --- .../Implementations/UserRepository.cs | 4 -- .../Implementations/ModelStateService.cs | 38 +++++++++++++++--- .../Services/Interfaces/IModelStateService.cs | 40 +++++++++++++++++++ .../IViewComponentTempDataService.cs | 10 ----- MVC/MVC/App_Start/StartupConfig.cs | 4 ++ .../ImportModelStateViewComponent.cs | 3 ++ .../ForgotPasswordController.cs | 7 +++- .../ForgotPasswordManual.cshtml | 3 +- .../ForgotPasswordPageTemplate.cshtml | 3 +- .../ForgotPasswordViewComponent.cs | 20 ++++++++-- .../ForgotPassword/ForgotPasswordViewModel.cs | 4 +- .../ForgottenPasswordReset.cshtml | 4 +- .../ForgottenPasswordResetController.cs | 10 ++++- .../ForgottenPasswordResetViewComponent.cs | 13 ++++-- .../ForgottenPasswordResetViewModel.cs | 23 ++++++++++- .../Features/Account/LogIn/LogInController.cs | 12 +++++- .../Features/Account/LogIn/LogInManual.cshtml | 3 +- .../Account/LogIn/LogInPageTemplate.cshtml | 3 +- .../Account/LogIn/LogInViewComponent.cs | 14 +++++-- .../Features/Account/LogIn/LogInViewModel.cs | 2 + .../Account/LogOut/LogOutManual.cshtml | 2 +- .../Account/LogOut/LogOutPageTemplate.cshtml | 2 +- .../Account/LogOut/LogOutViewComponent.cs | 6 ++- .../Account/MyAccount/MyAccountManual.cshtml | 2 +- .../MyAccount/MyAccountPageTemplate.cshtml | 2 +- .../MyAccount/MyAccountViewComponent.cs | 6 ++- .../Registration/RegisterPageTemplate.cshtml | 11 ----- .../Account/Registration/Registration.cshtml | 7 ++-- .../Registration/RegistrationController.cs | 22 ++++++---- .../Registration/RegistrationManual.cshtml | 7 +--- .../RegistrationPageTemplate.cshtml | 8 ++++ .../Registration/RegistrationViewComponent.cs | 29 +++++++++++--- .../Registration/RegistrationViewModel.cs | 16 +++++++- .../ResetPassword/ResetPasswordController.cs | 6 ++- .../ResetPassword/ResetPasswordManual.cshtml | 3 +- .../ResetPasswordPageTemplate.cshtml | 3 +- .../ResetPasswordViewComponent.cs | 14 +++++-- .../ResetPassword/ResetPasswordViewModel.cs | 3 +- .../Validation/PasswordValidationExtension.cs | 4 +- 39 files changed, 277 insertions(+), 96 deletions(-) create mode 100644 MVC/MVC.Libraries/Services/Interfaces/IModelStateService.cs delete mode 100644 MVC/MVC.Libraries/Services/Interfaces/IViewComponentTempDataService.cs delete mode 100644 MVC/MVC/Features/Account/Registration/RegisterPageTemplate.cshtml create mode 100644 MVC/MVC/Features/Account/Registration/RegistrationPageTemplate.cshtml diff --git a/MVC/MVC.Libraries/Repositories/Implementations/UserRepository.cs b/MVC/MVC.Libraries/Repositories/Implementations/UserRepository.cs index 1b8c72d..54d3640 100644 --- a/MVC/MVC.Libraries/Repositories/Implementations/UserRepository.cs +++ b/MVC/MVC.Libraries/Repositories/Implementations/UserRepository.cs @@ -55,7 +55,6 @@ public async Task 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) : null; @@ -73,7 +72,6 @@ public async Task 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) : null; @@ -91,7 +89,6 @@ public async Task 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) : null; @@ -112,7 +109,6 @@ public async Task 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) : null; diff --git a/MVC/MVC.Libraries/Services/Implementations/ModelStateService.cs b/MVC/MVC.Libraries/Services/Implementations/ModelStateService.cs index d529d44..7bb09ff 100644 --- a/MVC/MVC.Libraries/Services/Implementations/ModelStateService.cs +++ b/MVC/MVC.Libraries/Services/Implementations/ModelStateService.cs @@ -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; @@ -41,5 +38,36 @@ public void MergeModelState(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDict } } } + + public void StoreViewModel(ITempDataDictionary tempData, TModel viewModel) + { + tempData.Put($"GetViewModel_{typeof(TModel).FullName}", viewModel); + } + + public TModel GetViewModel(ITempDataDictionary tempData) + { + var obj = tempData.Get($"GetViewModel_{typeof(TModel).FullName}"); + return (obj != null ? (TModel) obj : default(TModel)); + } + + public void ClearViewModel(ITempDataDictionary tempData) + { + tempData.Remove($"GetViewModel_{typeof(TModel).FullName}"); + } + } + + public static class TempDataExtensions + { + public static void Put(this ITempDataDictionary tempData, string key, T value) + { + tempData[key] = JsonSerializer.Serialize(value); + } + + public static T Get(this ITempDataDictionary tempData, string key) + { + object o; + tempData.TryGetValue(key, out o); + return o == null ? default : JsonSerializer.Deserialize((string)o); + } } } diff --git a/MVC/MVC.Libraries/Services/Interfaces/IModelStateService.cs b/MVC/MVC.Libraries/Services/Interfaces/IModelStateService.cs new file mode 100644 index 0000000..ecebe8a --- /dev/null +++ b/MVC/MVC.Libraries/Services/Interfaces/IModelStateService.cs @@ -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 + { + /// + /// 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. + /// + /// + /// + void MergeModelState(ModelStateDictionary modelState, ITempDataDictionary tempData); + + /// + /// 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. + /// + /// + /// + /// + void StoreViewModel(ITempDataDictionary tempData, T viewModel); + + /// + /// Gets the View Model from the TempData, used in Post-Redirect-Get when redirecting back to the original request. + /// + /// + /// + /// + T GetViewModel(ITempDataDictionary tempData); + + /// + /// Removes the View Model from TempData, this should be called if you are redirecting away from the calling view. + /// + /// + /// + void ClearViewModel(ITempDataDictionary tempData); + } +} diff --git a/MVC/MVC.Libraries/Services/Interfaces/IViewComponentTempDataService.cs b/MVC/MVC.Libraries/Services/Interfaces/IViewComponentTempDataService.cs deleted file mode 100644 index 6979f11..0000000 --- a/MVC/MVC.Libraries/Services/Interfaces/IViewComponentTempDataService.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.ViewFeatures; -using MVCCaching; -namespace Generic.Services.Interfaces -{ - public interface IModelStateService : IService - { - void MergeModelState(ModelStateDictionary modelState, ITempDataDictionary tempData); - } -} diff --git a/MVC/MVC/App_Start/StartupConfig.cs b/MVC/MVC/App_Start/StartupConfig.cs index d35b093..5585261 100644 --- a/MVC/MVC/App_Start/StartupConfig.cs +++ b/MVC/MVC/App_Start/StartupConfig.cs @@ -29,6 +29,7 @@ using XperienceCommunity.Authorization; using XperienceCommunity.Localizer; using XperienceCommunity.PageBuilderUtilities; +using FluentValidation.AspNetCore; namespace Generic.App_Start { @@ -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) diff --git a/MVC/MVC/Components/ImportModelState/ImportModelStateViewComponent.cs b/MVC/MVC/Components/ImportModelState/ImportModelStateViewComponent.cs index 5985ff2..4763df3 100644 --- a/MVC/MVC/Components/ImportModelState/ImportModelStateViewComponent.cs +++ b/MVC/MVC/Components/ImportModelState/ImportModelStateViewComponent.cs @@ -1,5 +1,7 @@ using Generic.Services.Interfaces; using Microsoft.AspNetCore.Mvc; +using System; +using System.Text.Json; namespace Generic.Components.ImportModelState { @@ -25,6 +27,7 @@ public ImportModelStateViewComponent(IModelStateService modelStateService) public IViewComponentResult Invoke() { _modelStateService.MergeModelState(ModelState, TempData); + return Content(string.Empty); } } diff --git a/MVC/MVC/Features/Account/ForgotPassword/ForgotPasswordController.cs b/MVC/MVC/Features/Account/ForgotPassword/ForgotPasswordController.cs index 7acb4c0..aedd520 100644 --- a/MVC/MVC/Features/Account/ForgotPassword/ForgotPasswordController.cs +++ b/MVC/MVC/Features/Account/ForgotPassword/ForgotPasswordController.cs @@ -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; } /// @@ -68,6 +71,8 @@ public async Task ForgotPassword(ForgotPasswordViewModel model) model.Error = ex.Message; } + _modelStateService.StoreViewModel(TempData, model); + return Redirect(forgotPasswordUrl); } diff --git a/MVC/MVC/Features/Account/ForgotPassword/ForgotPasswordManual.cshtml b/MVC/MVC/Features/Account/ForgotPassword/ForgotPasswordManual.cshtml index 18ab29f..0c389bf 100644 --- a/MVC/MVC/Features/Account/ForgotPassword/ForgotPasswordManual.cshtml +++ b/MVC/MVC/Features/Account/ForgotPassword/ForgotPasswordManual.cshtml @@ -2,5 +2,4 @@ @section head{ } - - \ No newline at end of file + \ No newline at end of file diff --git a/MVC/MVC/Features/Account/ForgotPassword/ForgotPasswordPageTemplate.cshtml b/MVC/MVC/Features/Account/ForgotPassword/ForgotPasswordPageTemplate.cshtml index 9b41bad..a09fb08 100644 --- a/MVC/MVC/Features/Account/ForgotPassword/ForgotPasswordPageTemplate.cshtml +++ b/MVC/MVC/Features/Account/ForgotPassword/ForgotPasswordPageTemplate.cshtml @@ -2,5 +2,4 @@ @section head{ } - - \ No newline at end of file + \ No newline at end of file diff --git a/MVC/MVC/Features/Account/ForgotPassword/ForgotPasswordViewComponent.cs b/MVC/MVC/Features/Account/ForgotPassword/ForgotPasswordViewComponent.cs index 3f7aa79..3db8908 100644 --- a/MVC/MVC/Features/Account/ForgotPassword/ForgotPasswordViewComponent.cs +++ b/MVC/MVC/Features/Account/ForgotPassword/ForgotPasswordViewComponent.cs @@ -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; + } /// /// Uses the current page context to render meta data /// /// - 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(TempData) ?? new ForgotPasswordViewModel() + { + + }; return View("~/Features/Account/ForgotPassword/ForgotPassword.cshtml", model); } diff --git a/MVC/MVC/Features/Account/ForgotPassword/ForgotPasswordViewModel.cs b/MVC/MVC/Features/Account/ForgotPassword/ForgotPasswordViewModel.cs index f67d481..f7a173c 100644 --- a/MVC/MVC/Features/Account/ForgotPassword/ForgotPasswordViewModel.cs +++ b/MVC/MVC/Features/Account/ForgotPassword/ForgotPasswordViewModel.cs @@ -1,7 +1,9 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; namespace Generic.Features.Account.ForgotPassword { + [Serializable] public class ForgotPasswordViewModel { [Required] diff --git a/MVC/MVC/Features/Account/ForgottenPasswordReset/ForgottenPasswordReset.cshtml b/MVC/MVC/Features/Account/ForgottenPasswordReset/ForgottenPasswordReset.cshtml index e4509ea..7f6e8d6 100644 --- a/MVC/MVC/Features/Account/ForgottenPasswordReset/ForgottenPasswordReset.cshtml +++ b/MVC/MVC/Features/Account/ForgottenPasswordReset/ForgottenPasswordReset.cshtml @@ -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) {

Successful

diff --git a/MVC/MVC/Features/Account/ForgottenPasswordReset/ForgottenPasswordResetController.cs b/MVC/MVC/Features/Account/ForgottenPasswordReset/ForgottenPasswordResetController.cs index 30fca5f..1057c47 100644 --- a/MVC/MVC/Features/Account/ForgottenPasswordReset/ForgottenPasswordResetController.cs +++ b/MVC/MVC/Features/Account/ForgottenPasswordReset/ForgottenPasswordResetController.cs @@ -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; } /// @@ -80,6 +83,11 @@ public async Task 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); } diff --git a/MVC/MVC/Features/Account/ForgottenPasswordReset/ForgottenPasswordResetViewComponent.cs b/MVC/MVC/Features/Account/ForgottenPasswordReset/ForgottenPasswordResetViewComponent.cs index e0a18fc..935c9e5 100644 --- a/MVC/MVC/Features/Account/ForgottenPasswordReset/ForgottenPasswordResetViewComponent.cs +++ b/MVC/MVC/Features/Account/ForgottenPasswordReset/ForgottenPasswordResetViewComponent.cs @@ -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; @@ -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; } /// @@ -22,6 +26,9 @@ public ForgottenPasswordResetViewComponent(IHttpContextAccessor httpContextAcces /// public IViewComponentResult Invoke() { + // Merge Model State + _modelStateService.MergeModelState(ModelState, TempData); + // Get values from Query String Guid? userId = null; string token = null; @@ -37,7 +44,7 @@ public IViewComponentResult Invoke() token = queryToken.FirstOrDefault(); } - var model = new ForgottenPasswordResetViewModel() + var model = _modelStateService.GetViewModel(TempData) ?? new ForgottenPasswordResetViewModel() { UserID = userId ?? Guid.Empty, Token = token diff --git a/MVC/MVC/Features/Account/ForgottenPasswordReset/ForgottenPasswordResetViewModel.cs b/MVC/MVC/Features/Account/ForgottenPasswordReset/ForgottenPasswordResetViewModel.cs index b3d07c5..85e6499 100644 --- a/MVC/MVC/Features/Account/ForgottenPasswordReset/ForgottenPasswordResetViewModel.cs +++ b/MVC/MVC/Features/Account/ForgottenPasswordReset/ForgottenPasswordResetViewModel.cs @@ -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; @@ -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; } + + /// + /// Identityresult doesn't serialize/deserialize properly so need to use this as a toggle + /// + public bool? Succeeded { get; set; } public string LoginUrl { get; set; } } + + public class ForgottenPasswordResetViewModelValidator : AbstractValidator + { + public ForgottenPasswordResetViewModelValidator(ISiteSettingsRepository _siteSettingsRepository) + { + var passwordSettings = _siteSettingsRepository.GetPasswordPolicy(); + + RuleFor(model => model.Password).ValidPassword(passwordSettings); + RuleFor(model => model.PasswordConfirm).Equal(model => model.Password); + + } + } } diff --git a/MVC/MVC/Features/Account/LogIn/LogInController.cs b/MVC/MVC/Features/Account/LogIn/LogInController.cs index 2e75334..b27985e 100644 --- a/MVC/MVC/Features/Account/LogIn/LogInController.cs +++ b/MVC/MVC/Features/Account/LogIn/LogInController.cs @@ -25,13 +25,15 @@ public class LogInController : Controller private readonly ILogger _logger; private readonly SignInManager _signInManager; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IModelStateService _modelStateService; public LogInController(IUserRepository userRepository, ISiteSettingsRepository siteSettingsRepository, IUserService userService, ILogger logger, SignInManager signInManager, - IHttpContextAccessor httpContextAccessor) + IHttpContextAccessor httpContextAccessor, + IModelStateService modelStateService) { _userRepository = userRepository; _siteSettingsRepository = siteSettingsRepository; @@ -39,6 +41,7 @@ public LogInController(IUserRepository userRepository, _logger = logger; _signInManager = signInManager; _httpContextAccessor = httpContextAccessor; + _modelStateService = modelStateService; } /// @@ -83,6 +86,9 @@ public async Task LogIn(LogInViewModel model, [FromQuery] string r _logger.LogException(ex, nameof(LogInController), "Login", Description: $"For user {model.UserName}"); } + // Store results + _modelStateService.StoreViewModel(TempData, model); + // If the authentication was not successful, displays the sign-in form with an "Authentication failed" message if (model.Result != SignInResult.Success) { @@ -92,6 +98,10 @@ public async Task LogIn(LogInViewModel model, [FromQuery] string r if (await _siteSettingsRepository.GetAccountRedirectToAccountAfterLoginAsync()) { + // Redirectig away from Login, clear TempData so if they return to login it doesn't persist + _modelStateService.ClearViewModel(TempData); + ModelState.Clear(); + string redirectUrl = ""; // Try to get returnUrl from query if (!string.IsNullOrWhiteSpace(model.RedirectUrl)) diff --git a/MVC/MVC/Features/Account/LogIn/LogInManual.cshtml b/MVC/MVC/Features/Account/LogIn/LogInManual.cshtml index aa36661..7a2ac23 100644 --- a/MVC/MVC/Features/Account/LogIn/LogInManual.cshtml +++ b/MVC/MVC/Features/Account/LogIn/LogInManual.cshtml @@ -1,5 +1,4 @@ @section head { } - - + diff --git a/MVC/MVC/Features/Account/LogIn/LogInPageTemplate.cshtml b/MVC/MVC/Features/Account/LogIn/LogInPageTemplate.cshtml index e8aed2c..d522f5b 100644 --- a/MVC/MVC/Features/Account/LogIn/LogInPageTemplate.cshtml +++ b/MVC/MVC/Features/Account/LogIn/LogInPageTemplate.cshtml @@ -2,5 +2,4 @@ @section head { } - - \ No newline at end of file + \ No newline at end of file diff --git a/MVC/MVC/Features/Account/LogIn/LogInViewComponent.cs b/MVC/MVC/Features/Account/LogIn/LogInViewComponent.cs index a950d19..c87c733 100644 --- a/MVC/MVC/Features/Account/LogIn/LogInViewComponent.cs +++ b/MVC/MVC/Features/Account/LogIn/LogInViewComponent.cs @@ -21,22 +21,28 @@ public class LogInViewComponent : ViewComponent private readonly IHttpContextAccessor _httpContextAccessor; private readonly ISiteSettingsRepository _siteSettingsRepository; private readonly IPageContextRepository _pageContextRepository; + private readonly IModelStateService _modelStateService; public LogInViewComponent(IHttpContextAccessor httpContextAccessor, ISiteSettingsRepository siteSettingsRepository, - IPageContextRepository pageContextRepository) + IPageContextRepository pageContextRepository, + IModelStateService modelStateService) { _httpContextAccessor = httpContextAccessor; _siteSettingsRepository = siteSettingsRepository; _pageContextRepository = pageContextRepository; + _modelStateService = modelStateService; } /// /// Uses the current page context to render meta data /// /// - public async Task InvokeAsync(LogInViewModel model = null) + public async Task InvokeAsync() { + // Merge Model State + _modelStateService.MergeModelState(ModelState, TempData); + string redirectUrl = ""; // Try to get returnUrl from query if (_httpContextAccessor.HttpContext.Request.Query.TryGetValue("returnUrl", out StringValues queryReturnUrl) && queryReturnUrl.Any()) @@ -44,13 +50,15 @@ public async Task InvokeAsync(LogInViewModel model = null) redirectUrl = queryReturnUrl.FirstOrDefault(); } - model ??= new LogInViewModel() + var model = _modelStateService.GetViewModel(TempData) ?? new LogInViewModel() { RedirectUrl = redirectUrl, MyAccountUrl = await _siteSettingsRepository.GetAccountMyAccountUrlAsync(MyAccountController.GetUrl()), RegistrationUrl = await _siteSettingsRepository.GetAccountRegistrationUrlAsync(RegistrationController.GetUrl()), ForgotPassword = await _siteSettingsRepository.GetAccountForgotPasswordUrlAsync(ForgotPasswordController.GetUrl()) }; + + // Set this value fresh model.AlreadyLogedIn = !(await _pageContextRepository.IsEditModeAsync()) && User.Identity.IsAuthenticated; return View("~/Features/Account/LogIn/LogIn.cshtml", model); diff --git a/MVC/MVC/Features/Account/LogIn/LogInViewModel.cs b/MVC/MVC/Features/Account/LogIn/LogInViewModel.cs index 89cc4d5..a598d0c 100644 --- a/MVC/MVC/Features/Account/LogIn/LogInViewModel.cs +++ b/MVC/MVC/Features/Account/LogIn/LogInViewModel.cs @@ -1,9 +1,11 @@ using Microsoft.AspNetCore.Identity; +using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace Generic.Features.Account.LogIn { + [Serializable] public class LogInViewModel { diff --git a/MVC/MVC/Features/Account/LogOut/LogOutManual.cshtml b/MVC/MVC/Features/Account/LogOut/LogOutManual.cshtml index e599302..293dc1a 100644 --- a/MVC/MVC/Features/Account/LogOut/LogOutManual.cshtml +++ b/MVC/MVC/Features/Account/LogOut/LogOutManual.cshtml @@ -1,4 +1,4 @@ @section head { } - \ No newline at end of file + \ No newline at end of file diff --git a/MVC/MVC/Features/Account/LogOut/LogOutPageTemplate.cshtml b/MVC/MVC/Features/Account/LogOut/LogOutPageTemplate.cshtml index 8d19eb1..dd94b40 100644 --- a/MVC/MVC/Features/Account/LogOut/LogOutPageTemplate.cshtml +++ b/MVC/MVC/Features/Account/LogOut/LogOutPageTemplate.cshtml @@ -2,4 +2,4 @@ @section head { } - + diff --git a/MVC/MVC/Features/Account/LogOut/LogOutViewComponent.cs b/MVC/MVC/Features/Account/LogOut/LogOutViewComponent.cs index 7fbe4f2..048b2e0 100644 --- a/MVC/MVC/Features/Account/LogOut/LogOutViewComponent.cs +++ b/MVC/MVC/Features/Account/LogOut/LogOutViewComponent.cs @@ -9,10 +9,12 @@ public class LogOutViewComponent : ViewComponent /// Uses the current page context to render meta data /// /// - public IViewComponentResult Invoke(LogOutViewModel model = null) + public IViewComponentResult Invoke() { + // Nothing in View Model to need IModelStateService to restore + // Any retrieval here - model ??= new LogOutViewModel() + var model = new LogOutViewModel() { }; diff --git a/MVC/MVC/Features/Account/MyAccount/MyAccountManual.cshtml b/MVC/MVC/Features/Account/MyAccount/MyAccountManual.cshtml index c5455e3..be47554 100644 --- a/MVC/MVC/Features/Account/MyAccount/MyAccountManual.cshtml +++ b/MVC/MVC/Features/Account/MyAccount/MyAccountManual.cshtml @@ -2,4 +2,4 @@ @section head{ } - \ No newline at end of file + \ No newline at end of file diff --git a/MVC/MVC/Features/Account/MyAccount/MyAccountPageTemplate.cshtml b/MVC/MVC/Features/Account/MyAccount/MyAccountPageTemplate.cshtml index d08f585..6c20ead 100644 --- a/MVC/MVC/Features/Account/MyAccount/MyAccountPageTemplate.cshtml +++ b/MVC/MVC/Features/Account/MyAccount/MyAccountPageTemplate.cshtml @@ -2,4 +2,4 @@ @section head{ } - \ No newline at end of file + \ No newline at end of file diff --git a/MVC/MVC/Features/Account/MyAccount/MyAccountViewComponent.cs b/MVC/MVC/Features/Account/MyAccount/MyAccountViewComponent.cs index d2e877e..83997e0 100644 --- a/MVC/MVC/Features/Account/MyAccount/MyAccountViewComponent.cs +++ b/MVC/MVC/Features/Account/MyAccount/MyAccountViewComponent.cs @@ -10,9 +10,11 @@ public class MyAccountViewComponent : ViewComponent /// Uses the current page context to render meta data /// /// - public IViewComponentResult Invoke(MyAccountViewModel model) + public IViewComponentResult Invoke() { - model ??= new MyAccountViewModel(); + // Nothing for IModelStateService to be required + + var model = new MyAccountViewModel(); return View("~/Features/Account/MyAccount/MyAccount.cshtml", model); } diff --git a/MVC/MVC/Features/Account/Registration/RegisterPageTemplate.cshtml b/MVC/MVC/Features/Account/Registration/RegisterPageTemplate.cshtml deleted file mode 100644 index fe07f13..0000000 --- a/MVC/MVC/Features/Account/Registration/RegisterPageTemplate.cshtml +++ /dev/null @@ -1,11 +0,0 @@ -@model ComponentViewModel -@section head { - -} -@section bottom { - - - -} - - \ No newline at end of file diff --git a/MVC/MVC/Features/Account/Registration/Registration.cshtml b/MVC/MVC/Features/Account/Registration/Registration.cshtml index 9ebf091..5736a3e 100644 --- a/MVC/MVC/Features/Account/Registration/Registration.cshtml +++ b/MVC/MVC/Features/Account/Registration/Registration.cshtml @@ -1,7 +1,7 @@ @model Generic.Features.Account.Registration.RegistrationViewModel -@if (Model.RegisterationSuccessful.HasValue) +@if (Model.RegistrationSuccessful.HasValue) { - if (Model.RegisterationSuccessful.Value) + if (Model.RegistrationSuccessful.Value) {
@@ -21,7 +21,7 @@ else {
-
+

Create Account

@@ -30,6 +30,7 @@ else +
diff --git a/MVC/MVC/Features/Account/Registration/RegistrationController.cs b/MVC/MVC/Features/Account/Registration/RegistrationController.cs index b89999d..6377fc6 100644 --- a/MVC/MVC/Features/Account/Registration/RegistrationController.cs +++ b/MVC/MVC/Features/Account/Registration/RegistrationController.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc; using System; using System.Reflection; +using System.Text.Json; using System.Threading.Tasks; namespace Generic.Features.Account.Registration @@ -16,16 +17,20 @@ public class RegistrationController : Controller private readonly IUserService _userService; private readonly ILogger _logger; private readonly IUrlResolver _urlResolver; + private readonly IModelStateService _modelStateService; public RegistrationController(ISiteSettingsRepository siteSettingsRepository, IUserService userService, ILogger logger, - IUrlResolver urlResolver) + IUrlResolver urlResolver, + IModelStateService modelStateService + ) { _siteSettingsRepository = siteSettingsRepository; _userService = userService; _logger = logger; _urlResolver = urlResolver; + _modelStateService = modelStateService; } @@ -38,7 +43,7 @@ public RegistrationController(ISiteSettingsRepository siteSettingsRepository, [Route(_routeUrl)] public ActionResult Registration() { - return View("~/Features/Account/Register/RegisterManual.cshtml"); + return View("~/Features/Account/Registration/RegistrationManual.cshtml"); } /// @@ -51,7 +56,7 @@ public ActionResult Registration() [ExportModelState] public async Task Registration(RegistrationViewModel userAccountModel) { - var registerUrl = await _siteSettingsRepository.GetAccountRegistrationUrlAsync(GetUrl()); + var registrationUrl = await _siteSettingsRepository.GetAccountRegistrationUrlAsync(GetUrl()); // Ensure valid var passwordValid = await _userService.ValidatePasswordPolicyAsync(userAccountModel.Password); if (!passwordValid) @@ -60,7 +65,7 @@ public async Task Registration(RegistrationViewModel userAccountMo } if (!ModelState.IsValid || !passwordValid) { - return Redirect(registerUrl); + return Redirect(registrationUrl); } // Create a basic Kentico User and assign the portal ID @@ -71,16 +76,19 @@ public async Task Registration(RegistrationViewModel userAccountMo // Send confirmation email with registration link string confirmationUrl = await _siteSettingsRepository.GetAccountConfirmationUrlAsync(ConfirmationController.GetUrl()); await _userService.SendRegistrationConfirmationEmailAsync(newUser, _urlResolver.GetAbsoluteUrl(confirmationUrl)); - userAccountModel.RegisterationSuccessful = true; + userAccountModel.RegistrationSuccessful = true; } catch (Exception ex) { _logger.LogException(ex, nameof(RegistrationController), "Registration", Description: $"For User {userAccountModel.User}"); userAccountModel.RegistrationFailureMessage = ex.Message; - userAccountModel.RegisterationSuccessful = false; + userAccountModel.RegistrationSuccessful = false; } - return Redirect(registerUrl); + // Store view model for retrieval + _modelStateService.StoreViewModel(TempData, userAccountModel); + + return Redirect(registrationUrl); } diff --git a/MVC/MVC/Features/Account/Registration/RegistrationManual.cshtml b/MVC/MVC/Features/Account/Registration/RegistrationManual.cshtml index 68c80e9..534d50e 100644 --- a/MVC/MVC/Features/Account/Registration/RegistrationManual.cshtml +++ b/MVC/MVC/Features/Account/Registration/RegistrationManual.cshtml @@ -3,9 +3,6 @@ } @section bottom { - - - + @Generic.Features.Account.Registration.RegistrationViewComponent.FooterContent() } - - + diff --git a/MVC/MVC/Features/Account/Registration/RegistrationPageTemplate.cshtml b/MVC/MVC/Features/Account/Registration/RegistrationPageTemplate.cshtml new file mode 100644 index 0000000..c1626f0 --- /dev/null +++ b/MVC/MVC/Features/Account/Registration/RegistrationPageTemplate.cshtml @@ -0,0 +1,8 @@ +@model ComponentViewModel +@section head { + +} +@section bottom { + @Generic.Features.Account.Registration.RegistrationViewComponent.FooterContent() +} + \ No newline at end of file diff --git a/MVC/MVC/Features/Account/Registration/RegistrationViewComponent.cs b/MVC/MVC/Features/Account/Registration/RegistrationViewComponent.cs index f280c52..7b93170 100644 --- a/MVC/MVC/Features/Account/Registration/RegistrationViewComponent.cs +++ b/MVC/MVC/Features/Account/Registration/RegistrationViewComponent.cs @@ -1,22 +1,41 @@ -using Microsoft.AspNetCore.Mvc; +using Generic.Services.Interfaces; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc; namespace Generic.Features.Account.Registration { [ViewComponent] public class RegistrationViewComponent : ViewComponent { + private readonly IModelStateService _modelStateService; + + public RegistrationViewComponent(IModelStateService modelStateService ) + { + _modelStateService = modelStateService; + } /// /// Uses the current page context to render meta data /// /// - public IViewComponentResult Invoke(RegistrationViewModel model = null) + public IViewComponentResult Invoke() { - // Any retrieval here - model ??= new RegistrationViewModel() + _modelStateService.MergeModelState(ModelState, TempData); + + var model = _modelStateService.GetViewModel(TempData) ?? new RegistrationViewModel() { - + }; + + return View("~/Features/Account/Registration/Registration.cshtml", model); } + + public static IHtmlContent FooterContent() + { + return new HtmlString(@" + + + "); + } } } diff --git a/MVC/MVC/Features/Account/Registration/RegistrationViewModel.cs b/MVC/MVC/Features/Account/Registration/RegistrationViewModel.cs index b58c333..3bf169d 100644 --- a/MVC/MVC/Features/Account/Registration/RegistrationViewModel.cs +++ b/MVC/MVC/Features/Account/Registration/RegistrationViewModel.cs @@ -2,11 +2,13 @@ using Generic.Library.Validation; using Generic.Models.Account; using Generic.Repositories.Interfaces; +using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace Generic.Features.Account.Registration { + [Serializable] public class RegistrationViewModel { public BasicUser User { get; set; } @@ -20,7 +22,9 @@ public class RegistrationViewModel [DataType(DataType.Password)] [DisplayName("Confirm Password")] public string PasswordConfirm { get; set; } - public bool? RegisterationSuccessful { get; set; } + + public bool? RegistrationSuccessful { get; set; } + public string RegistrationFailureMessage { get; set; } public RegistrationViewModel() @@ -29,12 +33,20 @@ public RegistrationViewModel() } public class RegistrationViewModelValidator : AbstractValidator { - public RegistrationViewModelValidator(ISiteSettingsRepository _siteSettingsRepository) + public RegistrationViewModelValidator(ISiteSettingsRepository _siteSettingsRepository, IUserRepository userRepository) { var passwordSettings = _siteSettingsRepository.GetPasswordPolicy(); RuleFor(model => model.Password).ValidPassword(passwordSettings); RuleFor(model => model.PasswordConfirm).Equal(model => model.Password); + RuleFor(model => model.User.UserName).MustAsync(async (model, cancellationToken) => + { + return (await userRepository.GetUserAsync(model)) == null; + }).WithMessage("User already exists with that username"); + RuleFor(model => model.User.UserEmail).MustAsync(async (model, cancellationToken) => + { + return (await userRepository.GetUserByEmailAsync(model)) == null; + }).WithMessage("User already exists with that email address"); } } } \ No newline at end of file diff --git a/MVC/MVC/Features/Account/ResetPassword/ResetPasswordController.cs b/MVC/MVC/Features/Account/ResetPassword/ResetPasswordController.cs index 1999bed..a265777 100644 --- a/MVC/MVC/Features/Account/ResetPassword/ResetPasswordController.cs +++ b/MVC/MVC/Features/Account/ResetPassword/ResetPasswordController.cs @@ -16,16 +16,19 @@ public class ResetPasswordController : Controller private readonly ISiteSettingsRepository _siteSettingsRepository; private readonly IUserService _userService; private readonly ILogger _logger; + private readonly IModelStateService _modelStateService; public ResetPasswordController(IUserRepository userRepository, ISiteSettingsRepository siteSettingsRepository, IUserService userService, - ILogger logger) + ILogger logger, + IModelStateService modelStateService) { _userRepository = userRepository; _siteSettingsRepository = siteSettingsRepository; _userService = userService; _logger = logger; + _modelStateService = modelStateService; } /// @@ -72,6 +75,7 @@ public async Task ResetPassword(ResetPasswordViewModel model) model.Succeeded = false; model.Error = "An error occurred in changing the password."; } + _modelStateService.StoreViewModel(TempData, model); return Redirect(resetPasswordUrl); } diff --git a/MVC/MVC/Features/Account/ResetPassword/ResetPasswordManual.cshtml b/MVC/MVC/Features/Account/ResetPassword/ResetPasswordManual.cshtml index 83e4d98..d803e8d 100644 --- a/MVC/MVC/Features/Account/ResetPassword/ResetPasswordManual.cshtml +++ b/MVC/MVC/Features/Account/ResetPassword/ResetPasswordManual.cshtml @@ -2,5 +2,4 @@ @section head{ } - - \ No newline at end of file + \ No newline at end of file diff --git a/MVC/MVC/Features/Account/ResetPassword/ResetPasswordPageTemplate.cshtml b/MVC/MVC/Features/Account/ResetPassword/ResetPasswordPageTemplate.cshtml index 36ca0ec..fb31e90 100644 --- a/MVC/MVC/Features/Account/ResetPassword/ResetPasswordPageTemplate.cshtml +++ b/MVC/MVC/Features/Account/ResetPassword/ResetPasswordPageTemplate.cshtml @@ -2,5 +2,4 @@ @section head{ } - - \ No newline at end of file + \ No newline at end of file diff --git a/MVC/MVC/Features/Account/ResetPassword/ResetPasswordViewComponent.cs b/MVC/MVC/Features/Account/ResetPassword/ResetPasswordViewComponent.cs index c2af843..f08a837 100644 --- a/MVC/MVC/Features/Account/ResetPassword/ResetPasswordViewComponent.cs +++ b/MVC/MVC/Features/Account/ResetPassword/ResetPasswordViewComponent.cs @@ -1,17 +1,25 @@ -using Microsoft.AspNetCore.Mvc; +using Generic.Services.Interfaces; +using Microsoft.AspNetCore.Mvc; namespace Generic.Features.Account.ResetPassword { [ViewComponent] public class ResetPasswordViewComponent : ViewComponent { + private readonly IModelStateService _modelStateService; + + public ResetPasswordViewComponent(IModelStateService modelStateService) + { + _modelStateService = modelStateService; + } + /// /// Uses the current page context to render meta data /// /// - public IViewComponentResult Invoke(ResetPasswordViewModel model) + public IViewComponentResult Invoke() { - model ??= new ResetPasswordViewModel(); + var model = _modelStateService.GetViewModel(TempData) ?? new ResetPasswordViewModel(); return View("~/Features/Account/ResetPassword/ResetPassword.cshtml", model); } diff --git a/MVC/MVC/Features/Account/ResetPassword/ResetPasswordViewModel.cs b/MVC/MVC/Features/Account/ResetPassword/ResetPasswordViewModel.cs index 693f64e..096db7a 100644 --- a/MVC/MVC/Features/Account/ResetPassword/ResetPasswordViewModel.cs +++ b/MVC/MVC/Features/Account/ResetPassword/ResetPasswordViewModel.cs @@ -2,11 +2,13 @@ using Generic.Library.Validation; using Generic.Repositories.Interfaces; using Generic.Services.Interfaces; +using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace Generic.Features.Account.ResetPassword { + [Serializable] public class ResetPasswordViewModel { [Required] @@ -41,7 +43,6 @@ IUserService _userService RuleFor(model => model.Password).ValidPassword(passwordSettings); RuleFor(model => model.PasswordConfirm).Equal(model => model.Password); - RuleFor(model => model.CurrentPassword).ValidPassword(passwordSettings); RuleFor(model => model.CurrentPassword).MustAsync(async (password, thread) => { var user = await _userRepository.GetCurrentUserAsync(); diff --git a/MVC/MVC/Library/Validation/PasswordValidationExtension.cs b/MVC/MVC/Library/Validation/PasswordValidationExtension.cs index ef8a51d..a12396d 100644 --- a/MVC/MVC/Library/Validation/PasswordValidationExtension.cs +++ b/MVC/MVC/Library/Validation/PasswordValidationExtension.cs @@ -8,7 +8,7 @@ public static class PasswordValidationExtension public static IRuleBuilderOptions ValidPassword(this IRuleBuilder ruleBuilder, PasswordPolicySettings settings) { - string message = !string.IsNullOrWhiteSpace(settings.ViolationMessage) ? settings.ViolationMessage : string.Empty; + string message = !string.IsNullOrWhiteSpace(settings.ViolationMessage) ? settings.ViolationMessage : "Invalid Password"; var options = ruleBuilder.NotNull(); if (settings.UsePasswordPolicy) { @@ -22,7 +22,7 @@ public static IRuleBuilderOptions ValidPassword(this IRuleBuilder< } if(settings.NumNonAlphanumericChars > 0) { - options.Matches($"\\W{{{settings.NumNonAlphanumericChars},999}}").WithMessage(message); + options.Matches($"^(?=.{{{settings.NumNonAlphanumericChars},999}}\\W).*$").WithMessage(message); } } return options;