From 3ef39615463e3aa6d391d13dbff672d64f8714bc Mon Sep 17 00:00:00 2001 From: Ibrahim Munir-Zubair Date: Tue, 10 Aug 2021 14:05:49 +0100 Subject: [PATCH 1/9] HEEDLS-495 Implement 'Send welcome email' from View delegate page currently without an actual working link to set password --- .../Services/RegistrationService.cs | 51 ++++++++++++++++++- .../Delegates/ViewDelegateController.cs | 23 ++++++++- .../Delegates/ViewDelegate/Index.cshtml | 2 +- 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/DigitalLearningSolutions.Data/Services/RegistrationService.cs b/DigitalLearningSolutions.Data/Services/RegistrationService.cs index e1fdd74027..b0a3eb91c2 100644 --- a/DigitalLearningSolutions.Data/Services/RegistrationService.cs +++ b/DigitalLearningSolutions.Data/Services/RegistrationService.cs @@ -6,6 +6,7 @@ namespace DigitalLearningSolutions.Data.Services using DigitalLearningSolutions.Data.DataServices; using DigitalLearningSolutions.Data.Models.Email; using DigitalLearningSolutions.Data.Models.Register; + using DigitalLearningSolutions.Data.Models.User; using Microsoft.Extensions.Configuration; using MimeKit; @@ -20,15 +21,17 @@ bool refactoredTrackingSystemEnabled string RegisterDelegateByCentre(DelegateRegistrationModel delegateRegistrationModel); void RegisterCentreManager(RegistrationModel registrationModel); + + void GenerateAndSendDelegateWelcomeEmail(DelegateUserCard delegateUser); } public class RegistrationService : IRegistrationService { private readonly ICentresDataService centresDataService; + private readonly IConfiguration config; private readonly IEmailService emailService; private readonly IPasswordDataService passwordDataService; private readonly IRegistrationDataService registrationDataService; - private readonly IConfiguration config; public RegistrationService( IRegistrationDataService registrationDataService, @@ -115,6 +118,20 @@ public void RegisterCentreManager(RegistrationModel registrationModel) transaction.Complete(); } + public void GenerateAndSendDelegateWelcomeEmail(DelegateUserCard delegateUser) + { + using var transaction = new TransactionScope(); + + var resetPasswordEmail = GenerateWelcomeEmail( + delegateUser.EmailAddress!, + delegateUser.FirstName!, + delegateUser.LastName, + delegateUser.CentreName, + delegateUser.CandidateNumber + ); + emailService.SendEmail(resetPasswordEmail); + } + private void CreateDelegateAccountForAdmin(RegistrationModel registrationModel) { var delegateRegistrationModel = new DelegateRegistrationModel( @@ -169,5 +186,37 @@ bool refactoredTrackingSystemEnabled return new Email(emailSubject, body, emailAddress); } + + private Email GenerateWelcomeEmail( + string emailAddress, + string firstName, + string lastName, + string centreName, + string candidateNumber + ) + { + const string emailSubject = "Welcome to Digital Learning Solutions - Verify your Registration"; + var setPasswordUrl = $"{config["AppRootPath"]}"; // TODO + + BodyBuilder body = new BodyBuilder + { + TextBody = $@"Dear {firstName} {lastName}, + An administrator has registered your details to give you access to the Digital Learning Solutions (DLS) platform under the centre {centreName}. + You have been assigned the unique DLS delegate number {candidateNumber}. + To complete your registration and access your Digital Learning Solutions content, please follow this link:{setPasswordUrl} + Note that this link can only be used once. + Please don't reply to this email as it has been automatically generated.", + HtmlBody = $@" +

Dear {firstName},

+

An administrator has registered your details to give you access to the Digital Learning Solutions (DLS) platform under the centre {centreName}.

+

You have been assigned the unique DLS delegate number {candidateNumber}.

+

To complete your registration and access your Digital Learning Solutions content, please follow this link: {setPasswordUrl}

+

Note that this link can only be used once.

+

Please don't reply to this email as it has been automatically generated.

+ " + }; + + return new Email(emailSubject, body, emailAddress); + } } } diff --git a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/ViewDelegateController.cs b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/ViewDelegateController.cs index a2b1e9ac2d..462dcb828e 100644 --- a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/ViewDelegateController.cs +++ b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/ViewDelegateController.cs @@ -1,8 +1,9 @@ -namespace DigitalLearningSolutions.Web.Controllers.TrackingSystem.Delegates +namespace DigitalLearningSolutions.Web.Controllers.TrackingSystem.Delegates { using System.Linq; using DigitalLearningSolutions.Data.DataServices.UserDataService; using DigitalLearningSolutions.Data.Services; + using DigitalLearningSolutions.Web.Extensions; using DigitalLearningSolutions.Web.Helpers; using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates; using Microsoft.AspNetCore.Authorization; @@ -16,17 +17,20 @@ public class ViewDelegateController : Controller { private readonly ICourseService courseService; private readonly CustomPromptHelper customPromptHelper; + private readonly IRegistrationService registrationService; private readonly IUserDataService userDataService; public ViewDelegateController( IUserDataService userDataService, CustomPromptHelper customPromptHelper, - ICourseService courseService + ICourseService courseService, + IRegistrationService registrationService ) { this.userDataService = userDataService; this.customPromptHelper = customPromptHelper; this.courseService = courseService; + this.registrationService = registrationService; } public IActionResult Index(int delegateId) @@ -48,5 +52,20 @@ public IActionResult Index(int delegateId) var model = new ViewDelegateViewModel(delegateInfoViewModel, courseInfoViewModels); return View(model); } + + [Route("SendWelcomeEmail")] + public IActionResult SendWelcomeEmail(int delegateId) + { + var centreId = User.GetCentreId(); + + var delegateUser = userDataService.GetDelegateUserCardById(delegateId); + if (delegateUser == null || delegateUser.CentreId != centreId) + { + return new NotFoundResult(); + } + + registrationService.GenerateAndSendDelegateWelcomeEmail(delegateUser!); + + } } } diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/ViewDelegate/Index.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/ViewDelegate/Index.cshtml index c43cbe89c2..97c8e29bb6 100644 --- a/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/ViewDelegate/Index.cshtml +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/ViewDelegate/Index.cshtml @@ -29,7 +29,7 @@ @if (Model.DelegateInfo.IsActive) {
- + Send welcome email
From 47949a50930995ab31d1be8c91f410e85558af28 Mon Sep 17 00:00:00 2001 From: Ibrahim Munir-Zubair Date: Tue, 10 Aug 2021 14:06:12 +0100 Subject: [PATCH 2/9] HEEDLS-495 Implement 'Welcome email sent' page --- .../Delegates/ViewDelegateController.cs | 15 ++++++++++- DigitalLearningSolutions.Web/Startup.cs | 2 ++ .../Delegates/WelcomeEmailSentViewModel.cs | 18 +++++++++++++ .../ViewDelegate/WelcomeEmailSent.cshtml | 26 +++++++++++++++++++ 4 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/WelcomeEmailSentViewModel.cs create mode 100644 DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/ViewDelegate/WelcomeEmailSent.cshtml diff --git a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/ViewDelegateController.cs b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/ViewDelegateController.cs index 462dcb828e..3e426d206c 100644 --- a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/ViewDelegateController.cs +++ b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/ViewDelegateController.cs @@ -1,10 +1,11 @@ -namespace DigitalLearningSolutions.Web.Controllers.TrackingSystem.Delegates +namespace DigitalLearningSolutions.Web.Controllers.TrackingSystem.Delegates { using System.Linq; using DigitalLearningSolutions.Data.DataServices.UserDataService; using DigitalLearningSolutions.Data.Services; using DigitalLearningSolutions.Web.Extensions; using DigitalLearningSolutions.Web.Helpers; + using DigitalLearningSolutions.Web.ServiceFilter; using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -66,6 +67,18 @@ public IActionResult SendWelcomeEmail(int delegateId) registrationService.GenerateAndSendDelegateWelcomeEmail(delegateUser!); + TempData.Set(new WelcomeEmailSentViewModel(delegateUser)); + + return RedirectToAction("WelcomeEmailSent", new { delegateId = delegateUser.Id }); + } + + [HttpGet] + [Route("WelcomeEmailSent")] + [ServiceFilter(typeof(RedirectEmptySessionData))] + public IActionResult WelcomeEmailSent() + { + var model = TempData.Get()!; + return View("WelcomeEmailSent", model); } } } diff --git a/DigitalLearningSolutions.Web/Startup.cs b/DigitalLearningSolutions.Web/Startup.cs index d59448777c..3798997f90 100644 --- a/DigitalLearningSolutions.Web/Startup.cs +++ b/DigitalLearningSolutions.Web/Startup.cs @@ -17,6 +17,7 @@ namespace DigitalLearningSolutions.Web using DigitalLearningSolutions.Web.ModelBinders; using DigitalLearningSolutions.Web.Models; using DigitalLearningSolutions.Web.ServiceFilter; + using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates; using FluentMigrator.Runner; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; @@ -206,6 +207,7 @@ private static void RegisterWebServiceFilters(IServiceCollection services) services.AddScoped>>(); services.AddScoped>(); services.AddScoped>(); + services.AddScoped>(); } public void Configure(IApplicationBuilder app, IMigrationRunner migrationRunner, IFeatureManager featureManager) diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/WelcomeEmailSentViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/WelcomeEmailSentViewModel.cs new file mode 100644 index 0000000000..407b424547 --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/WelcomeEmailSentViewModel.cs @@ -0,0 +1,18 @@ +namespace DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates +{ + using DigitalLearningSolutions.Data.Models.User; + + public class WelcomeEmailSentViewModel + { + public WelcomeEmailSentViewModel() { } + + public WelcomeEmailSentViewModel(DelegateUserCard delegateUser) + { + Name = delegateUser.SearchableName; + CandidateNumber = delegateUser.CandidateNumber; + } + + public string Name { get; set; } + public string CandidateNumber { get; set; } + } +} diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/ViewDelegate/WelcomeEmailSent.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/ViewDelegate/WelcomeEmailSent.cshtml new file mode 100644 index 0000000000..a7a17be36a --- /dev/null +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/ViewDelegate/WelcomeEmailSent.cshtml @@ -0,0 +1,26 @@ +@inject IConfiguration Configuration +@using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates +@using Microsoft.Extensions.Configuration +@model WelcomeEmailSentViewModel + +@{ + ViewData["Title"] = "Welcome email sent"; + ViewData["Application"] = "Tracking System"; + ViewData["HeaderPath"] = $"{Configuration["AppRootPath"]}/TrackingSystem/Centre/Dashboard"; + ViewData["HeaderPathName"] = "Tracking System"; +} + +@section NavMenuItems { + +} + +@section NavBreadcrumbs { + +} + +
+
+

Welcome email sent

+

A welcome message has been sent to @Model.Name (@Model.CandidateNumber), inviting them to set their password and confirm their registration.

+
+
From edeaf10eb3a885a6c3528823a980e89dd798ba63 Mon Sep 17 00:00:00 2001 From: Ibrahim Munir-Zubair Date: Tue, 10 Aug 2021 14:14:30 +0100 Subject: [PATCH 3/9] HEEDLS-495 Add breadcrumbs to View delegate page And fix incorrect tag-text bug! --- .../Delegates/WelcomeEmailSentViewModel.cs | 4 +++- .../Delegates/ViewDelegate/Index.cshtml | 2 +- .../ViewDelegate/WelcomeEmailSent.cshtml | 2 +- .../_BreadcrumbsToViewDelegatePage.cshtml | 17 +++++++++++++++++ 4 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/ViewDelegate/_BreadcrumbsToViewDelegatePage.cshtml diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/WelcomeEmailSentViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/WelcomeEmailSentViewModel.cs index 407b424547..500010b769 100644 --- a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/WelcomeEmailSentViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/WelcomeEmailSentViewModel.cs @@ -8,10 +8,12 @@ public WelcomeEmailSentViewModel() { } public WelcomeEmailSentViewModel(DelegateUserCard delegateUser) { + Id = delegateUser.Id; Name = delegateUser.SearchableName; CandidateNumber = delegateUser.CandidateNumber; } - + + public int Id { get; set; } public string Name { get; set; } public string CandidateNumber { get; set; } } diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/ViewDelegate/Index.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/ViewDelegate/Index.cshtml index 97c8e29bb6..82dd8b6392 100644 --- a/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/ViewDelegate/Index.cshtml +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/ViewDelegate/Index.cshtml @@ -48,7 +48,7 @@ @if (Model.DelegateInfo.IsAdmin) {
- @(Model.PasswordTagName) + Admin
} diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/ViewDelegate/WelcomeEmailSent.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/ViewDelegate/WelcomeEmailSent.cshtml index a7a17be36a..b5323442fe 100644 --- a/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/ViewDelegate/WelcomeEmailSent.cshtml +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/ViewDelegate/WelcomeEmailSent.cshtml @@ -15,7 +15,7 @@ } @section NavBreadcrumbs { - + }
diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/ViewDelegate/_BreadcrumbsToViewDelegatePage.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/ViewDelegate/_BreadcrumbsToViewDelegatePage.cshtml new file mode 100644 index 0000000000..c971e82682 --- /dev/null +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/ViewDelegate/_BreadcrumbsToViewDelegatePage.cshtml @@ -0,0 +1,17 @@ +@model int + + From 3084b9da9ae7090a871794d6d384cbd2f0bd94db Mon Sep 17 00:00:00 2001 From: Ibrahim Munir-Zubair Date: Tue, 10 Aug 2021 15:37:10 +0100 Subject: [PATCH 4/9] HEEDLS-495 Create SetPassword controller/views based off ResetPassword --- .../Controllers/SetPasswordController.cs | 9 ++++ .../Views/SetPassword/Error.cshtml | 13 +++++ .../Views/SetPassword/Index.cshtml | 50 +++++++++++++++++++ .../Views/SetPassword/Success.cshtml | 16 ++++++ 4 files changed, 88 insertions(+) create mode 100644 DigitalLearningSolutions.Web/Controllers/SetPasswordController.cs create mode 100644 DigitalLearningSolutions.Web/Views/SetPassword/Error.cshtml create mode 100644 DigitalLearningSolutions.Web/Views/SetPassword/Index.cshtml create mode 100644 DigitalLearningSolutions.Web/Views/SetPassword/Success.cshtml diff --git a/DigitalLearningSolutions.Web/Controllers/SetPasswordController.cs b/DigitalLearningSolutions.Web/Controllers/SetPasswordController.cs new file mode 100644 index 0000000000..4e1d9db68e --- /dev/null +++ b/DigitalLearningSolutions.Web/Controllers/SetPasswordController.cs @@ -0,0 +1,9 @@ +namespace DigitalLearningSolutions.Web.Controllers +{ + using DigitalLearningSolutions.Data.Services; + + public class SetPasswordController : ResetPasswordController + { + public SetPasswordController(IPasswordResetService passwordResetService, IPasswordService passwordService) : base(passwordResetService, passwordService) { } + } +} diff --git a/DigitalLearningSolutions.Web/Views/SetPassword/Error.cshtml b/DigitalLearningSolutions.Web/Views/SetPassword/Error.cshtml new file mode 100644 index 0000000000..c8369bd2ef --- /dev/null +++ b/DigitalLearningSolutions.Web/Views/SetPassword/Error.cshtml @@ -0,0 +1,13 @@ +@{ + ViewData["Title"] = "Set Password: Error"; +} + +@section NavMenuItems { + +} + +

Something went wrong...

+

+ There was a problem resetting your password. + The set password link is only valid once, and expires after 2 hours. +

diff --git a/DigitalLearningSolutions.Web/Views/SetPassword/Index.cshtml b/DigitalLearningSolutions.Web/Views/SetPassword/Index.cshtml new file mode 100644 index 0000000000..65346d7151 --- /dev/null +++ b/DigitalLearningSolutions.Web/Views/SetPassword/Index.cshtml @@ -0,0 +1,50 @@ +@using DigitalLearningSolutions.Web.ViewModels.Common +@model ConfirmPasswordViewModel + +@{ + var errorHasOccurred = !ViewData.ModelState.IsValid; + ViewData["Title"] = errorHasOccurred ? "Error: Set Password" : "Set Password"; +} + +@section NavMenuItems { + +} + +
+
+
+ + @if (errorHasOccurred) + { + + } + +
+

Set password

+

+ Use the form below to set the password for delegate and admin accounts associated with your email address. +

+
+ + + + + + + +
+
diff --git a/DigitalLearningSolutions.Web/Views/SetPassword/Success.cshtml b/DigitalLearningSolutions.Web/Views/SetPassword/Success.cshtml new file mode 100644 index 0000000000..1af37feac9 --- /dev/null +++ b/DigitalLearningSolutions.Web/Views/SetPassword/Success.cshtml @@ -0,0 +1,16 @@ +@{ + ViewData["Title"] = "Password Set Successfully"; +} + +@section NavMenuItems { + +} + +
+
+

Set password

+
Your password was successfully set.
+ + +
+
From 2aad6e90750a596c1ce1633cef6fdaab950336d9 Mon Sep 17 00:00:00 2001 From: Ibrahim Munir-Zubair Date: Tue, 10 Aug 2021 15:39:46 +0100 Subject: [PATCH 5/9] HEEDLS-495 Generate set password link and include in welcome email --- .../Services/RegistrationServiceTests.cs | 3 +++ .../Services/PasswordResetService.cs | 3 ++- .../Services/RegistrationService.cs | 18 +++++++++++++----- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/DigitalLearningSolutions.Data.Tests/Services/RegistrationServiceTests.cs b/DigitalLearningSolutions.Data.Tests/Services/RegistrationServiceTests.cs index 8511437b4f..94a5d1a7d5 100644 --- a/DigitalLearningSolutions.Data.Tests/Services/RegistrationServiceTests.cs +++ b/DigitalLearningSolutions.Data.Tests/Services/RegistrationServiceTests.cs @@ -53,6 +53,7 @@ public class RegistrationServiceTests private ICentresDataService centresDataService = null!; private IEmailService emailService = null!; private IPasswordDataService passwordDataService = null!; + private IPasswordResetService passwordResetService = null!; private IRegistrationDataService registrationDataService = null!; private IRegistrationService registrationService = null!; private IConfiguration config = null!; @@ -62,6 +63,7 @@ public void Setup() { registrationDataService = A.Fake(); passwordDataService = A.Fake(); + passwordResetService = A.Fake(); emailService = A.Fake(); centresDataService = A.Fake(); config = A.Fake(); @@ -82,6 +84,7 @@ public void Setup() registrationService = new RegistrationService( registrationDataService, passwordDataService, + passwordResetService, emailService, centresDataService, config diff --git a/DigitalLearningSolutions.Data/Services/PasswordResetService.cs b/DigitalLearningSolutions.Data/Services/PasswordResetService.cs index bdf46ed1c2..6ee37cbbc1 100644 --- a/DigitalLearningSolutions.Data/Services/PasswordResetService.cs +++ b/DigitalLearningSolutions.Data/Services/PasswordResetService.cs @@ -19,6 +19,7 @@ public interface IPasswordResetService Task EmailAndResetPasswordHashAreValidAsync(string emailAddress, string resetHash); void GenerateAndSendPasswordResetLink(string emailAddress, string baseUrl); Task InvalidateResetPasswordForEmailAsync(string email); + public string GenerateResetPasswordHash(User user); } public class PasswordResetService : IPasswordResetService @@ -82,7 +83,7 @@ public async Task InvalidateResetPasswordForEmailAsync(string email) } } - private string GenerateResetPasswordHash(User user) + public string GenerateResetPasswordHash(User user) { string hash = Guid.NewGuid().ToString(); diff --git a/DigitalLearningSolutions.Data/Services/RegistrationService.cs b/DigitalLearningSolutions.Data/Services/RegistrationService.cs index b0a3eb91c2..7345427483 100644 --- a/DigitalLearningSolutions.Data/Services/RegistrationService.cs +++ b/DigitalLearningSolutions.Data/Services/RegistrationService.cs @@ -31,11 +31,13 @@ public class RegistrationService : IRegistrationService private readonly IConfiguration config; private readonly IEmailService emailService; private readonly IPasswordDataService passwordDataService; + private readonly IPasswordResetService passwordResetService; private readonly IRegistrationDataService registrationDataService; public RegistrationService( IRegistrationDataService registrationDataService, IPasswordDataService passwordDataService, + IPasswordResetService passwordResetService, IEmailService emailService, ICentresDataService centresDataService, IConfiguration config @@ -43,6 +45,7 @@ IConfiguration config { this.registrationDataService = registrationDataService; this.passwordDataService = passwordDataService; + this.passwordResetService = passwordResetService; this.emailService = emailService; this.centresDataService = centresDataService; this.config = config; @@ -122,14 +125,18 @@ public void GenerateAndSendDelegateWelcomeEmail(DelegateUserCard delegateUser) { using var transaction = new TransactionScope(); + string setPasswordHash = passwordResetService.GenerateResetPasswordHash(delegateUser); var resetPasswordEmail = GenerateWelcomeEmail( - delegateUser.EmailAddress!, + delegateUser.EmailAddress!.Trim(), delegateUser.FirstName!, delegateUser.LastName, delegateUser.CentreName, - delegateUser.CandidateNumber + delegateUser.CandidateNumber, + setPasswordHash ); emailService.SendEmail(resetPasswordEmail); + + transaction.Complete(); } private void CreateDelegateAccountForAdmin(RegistrationModel registrationModel) @@ -192,12 +199,13 @@ private Email GenerateWelcomeEmail( string firstName, string lastName, string centreName, - string candidateNumber + string candidateNumber, + string setPasswordHash ) { const string emailSubject = "Welcome to Digital Learning Solutions - Verify your Registration"; - var setPasswordUrl = $"{config["AppRootPath"]}"; // TODO - + var setPasswordUrl = $"{config["AppRootPath"]}/SetPassword?code={setPasswordHash}&email={emailAddress}"; + BodyBuilder body = new BodyBuilder { TextBody = $@"Dear {firstName} {lastName}, From 413d02ea52c34f157b14c97b3e7d94036a626afc Mon Sep 17 00:00:00 2001 From: Ibrahim Munir-Zubair Date: Fri, 13 Aug 2021 15:31:34 +0100 Subject: [PATCH 6/9] HEEDLS-495 Refactor into SetPasswordControllerBase; variable renaming --- .../Services/RegistrationService.cs | 4 +- .../Controllers/ResetPasswordController.cs | 78 +----------------- .../Controllers/SetPasswordController.cs | 5 +- .../Controllers/SetPasswordControllerBase.cs | 82 +++++++++++++++++++ 4 files changed, 90 insertions(+), 79 deletions(-) create mode 100644 DigitalLearningSolutions.Web/Controllers/SetPasswordControllerBase.cs diff --git a/DigitalLearningSolutions.Data/Services/RegistrationService.cs b/DigitalLearningSolutions.Data/Services/RegistrationService.cs index 7345427483..e22324e2fe 100644 --- a/DigitalLearningSolutions.Data/Services/RegistrationService.cs +++ b/DigitalLearningSolutions.Data/Services/RegistrationService.cs @@ -126,7 +126,7 @@ public void GenerateAndSendDelegateWelcomeEmail(DelegateUserCard delegateUser) using var transaction = new TransactionScope(); string setPasswordHash = passwordResetService.GenerateResetPasswordHash(delegateUser); - var resetPasswordEmail = GenerateWelcomeEmail( + var welcomeEmail = GenerateWelcomeEmail( delegateUser.EmailAddress!.Trim(), delegateUser.FirstName!, delegateUser.LastName, @@ -134,7 +134,7 @@ public void GenerateAndSendDelegateWelcomeEmail(DelegateUserCard delegateUser) delegateUser.CandidateNumber, setPasswordHash ); - emailService.SendEmail(resetPasswordEmail); + emailService.SendEmail(welcomeEmail); transaction.Complete(); } diff --git a/DigitalLearningSolutions.Web/Controllers/ResetPasswordController.cs b/DigitalLearningSolutions.Web/Controllers/ResetPasswordController.cs index d7dc3a6acb..4f0067a3db 100644 --- a/DigitalLearningSolutions.Web/Controllers/ResetPasswordController.cs +++ b/DigitalLearningSolutions.Web/Controllers/ResetPasswordController.cs @@ -1,82 +1,10 @@ namespace DigitalLearningSolutions.Web.Controllers { - using System.Threading.Tasks; using DigitalLearningSolutions.Data.Services; - using DigitalLearningSolutions.Web.Extensions; - using DigitalLearningSolutions.Web.Models; - using DigitalLearningSolutions.Web.ServiceFilter; - using DigitalLearningSolutions.Web.ViewModels.Common; - using Microsoft.AspNetCore.Mvc; - public class ResetPasswordController : Controller + public class ResetPasswordController : SetPasswordControllerBase { - private readonly IPasswordResetService passwordResetService; - private readonly IPasswordService passwordService; - - public ResetPasswordController(IPasswordResetService passwordResetService, IPasswordService passwordService) - { - this.passwordResetService = passwordResetService; - this.passwordService = passwordService; - } - - [HttpGet] - public async Task Index(string email, string code) - { - if (User.Identity.IsAuthenticated) - { - return RedirectToAction("Index", "Home"); - } - - if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(code)) - { - return RedirectToAction("Index", "Login"); - } - - var hashIsValid = await passwordResetService.EmailAndResetPasswordHashAreValidAsync(email, code); - - TempData.Set(new ResetPasswordData(email, code)); - - if (!hashIsValid) - { - return RedirectToAction("Error"); - } - - return View(new ConfirmPasswordViewModel()); - } - - [HttpPost] - [ServiceFilter(typeof(RedirectEmptySessionData))] - public async Task Index(ConfirmPasswordViewModel viewModel) - { - var resetPasswordData = TempData.Peek()!; - - var hashIsValid = await passwordResetService.EmailAndResetPasswordHashAreValidAsync( - resetPasswordData.Email, - resetPasswordData.ResetPasswordHash); - - if (!hashIsValid) - { - TempData.Clear(); - return RedirectToAction("Error"); - } - - if (!ModelState.IsValid) - { - return View(viewModel); - } - - await passwordResetService.InvalidateResetPasswordForEmailAsync(resetPasswordData.Email); - await passwordService.ChangePasswordAsync(resetPasswordData.Email, viewModel.Password!); - - TempData.Clear(); - - return View("Success"); - } - - [HttpGet] - public IActionResult Error() - { - return View(); - } + public ResetPasswordController(IPasswordResetService passwordResetService, IPasswordService passwordService) : + base(passwordResetService, passwordService) { } } } diff --git a/DigitalLearningSolutions.Web/Controllers/SetPasswordController.cs b/DigitalLearningSolutions.Web/Controllers/SetPasswordController.cs index 4e1d9db68e..ac8d74dcda 100644 --- a/DigitalLearningSolutions.Web/Controllers/SetPasswordController.cs +++ b/DigitalLearningSolutions.Web/Controllers/SetPasswordController.cs @@ -2,8 +2,9 @@ namespace DigitalLearningSolutions.Web.Controllers { using DigitalLearningSolutions.Data.Services; - public class SetPasswordController : ResetPasswordController + public class SetPasswordController : SetPasswordControllerBase { - public SetPasswordController(IPasswordResetService passwordResetService, IPasswordService passwordService) : base(passwordResetService, passwordService) { } + public SetPasswordController(IPasswordResetService passwordResetService, IPasswordService passwordService) : + base(passwordResetService, passwordService) { } } } diff --git a/DigitalLearningSolutions.Web/Controllers/SetPasswordControllerBase.cs b/DigitalLearningSolutions.Web/Controllers/SetPasswordControllerBase.cs new file mode 100644 index 0000000000..aa50db4513 --- /dev/null +++ b/DigitalLearningSolutions.Web/Controllers/SetPasswordControllerBase.cs @@ -0,0 +1,82 @@ +namespace DigitalLearningSolutions.Web.Controllers +{ + using System.Threading.Tasks; + using DigitalLearningSolutions.Data.Services; + using DigitalLearningSolutions.Web.Extensions; + using DigitalLearningSolutions.Web.Models; + using DigitalLearningSolutions.Web.ServiceFilter; + using DigitalLearningSolutions.Web.ViewModels.Common; + using Microsoft.AspNetCore.Mvc; + + public class SetPasswordControllerBase : Controller + { + private readonly IPasswordResetService passwordResetService; + private readonly IPasswordService passwordService; + + public SetPasswordControllerBase(IPasswordResetService passwordResetService, IPasswordService passwordService) + { + this.passwordResetService = passwordResetService; + this.passwordService = passwordService; + } + + [HttpGet] + public async Task Index(string email, string code) + { + if (User.Identity.IsAuthenticated) + { + return RedirectToAction("Index", "Home"); + } + + if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(code)) + { + return RedirectToAction("Index", "Login"); + } + + var hashIsValid = await passwordResetService.EmailAndResetPasswordHashAreValidAsync(email, code); + + TempData.Set(new ResetPasswordData(email, code)); + + if (!hashIsValid) + { + return RedirectToAction("Error"); + } + + return View(new ConfirmPasswordViewModel()); + } + + [HttpPost] + [ServiceFilter(typeof(RedirectEmptySessionData))] + public async Task Index(ConfirmPasswordViewModel viewModel) + { + var resetPasswordData = TempData.Peek()!; + + var hashIsValid = await passwordResetService.EmailAndResetPasswordHashAreValidAsync( + resetPasswordData.Email, + resetPasswordData.ResetPasswordHash); + + if (!hashIsValid) + { + TempData.Clear(); + return RedirectToAction("Error"); + } + + if (!ModelState.IsValid) + { + return View(viewModel); + } + + await passwordResetService.InvalidateResetPasswordForEmailAsync(resetPasswordData.Email); + await passwordService.ChangePasswordAsync(resetPasswordData.Email, viewModel.Password!); + + TempData.Clear(); + + return View("Success"); + } + + [HttpGet] + public IActionResult Error() + { + return View(); + } + } +} From 56c60ceedbcd7cad5d9d86800efc712a7a43caba Mon Sep 17 00:00:00 2001 From: Alex Jackson Date: Tue, 17 Aug 2021 12:14:02 +0100 Subject: [PATCH 7/9] HEEDLS-495 Refactor and add new unit tests --- .../Helpers/ResetPasswordHelperTests.cs | 34 ++- .../Services/PasswordResetServiceTests.cs | 79 ++++-- .../TestHelpers/EmailTestHelper.cs | 2 +- .../Helpers/ResetPasswordHelpers.cs | 7 +- .../Services/DelegateApprovalsService.cs | 4 +- .../Services/NotificationService.cs | 2 +- .../Services/PasswordResetService.cs | 117 +++++++-- .../Services/RegistrationService.cs | 56 +--- .../ResetPasswordControllerTests.cs | 30 +-- .../Controllers/SetPasswordControllerTests.cs | 240 ++++++++++++++++++ .../Controllers/ResetPasswordController.cs | 61 +++++ .../Controllers/SetPasswordController.cs | 63 +++++ .../Controllers/SetPasswordControllerBase.cs | 90 +++---- .../Delegates/ViewDelegateController.cs | 19 +- .../Views/SetPassword/Error.cshtml | 4 +- 15 files changed, 622 insertions(+), 186 deletions(-) create mode 100644 DigitalLearningSolutions.Web.Tests/Controllers/SetPasswordControllerTests.cs diff --git a/DigitalLearningSolutions.Data.Tests/Helpers/ResetPasswordHelperTests.cs b/DigitalLearningSolutions.Data.Tests/Helpers/ResetPasswordHelperTests.cs index 9d67d9c3c9..6db5f5b56d 100644 --- a/DigitalLearningSolutions.Data.Tests/Helpers/ResetPasswordHelperTests.cs +++ b/DigitalLearningSolutions.Data.Tests/Helpers/ResetPasswordHelperTests.cs @@ -21,7 +21,7 @@ public void Reset_Password_Is_Valid_119_Minutes_After_Creation() .With(rp => rp.PasswordResetDateTime = createTime).Build(); // When - var resetIsValid = resetPassword.IsStillValidAt(createTime + TimeSpan.FromMinutes(119)); + var resetIsValid = resetPassword.IsStillValidAt(createTime + TimeSpan.FromMinutes(119), ResetPasswordHelpers.ResetPasswordHashExpiryTime); // Then resetIsValid.Should().BeTrue(); @@ -36,7 +36,37 @@ public void Reset_Password_Is_Invalid_121_Minutes_After_Creation() .With(rp => rp.PasswordResetDateTime = createTime).Build(); // When - var resetIsValid = resetPassword.IsStillValidAt(createTime + TimeSpan.FromMinutes(121)); + var resetIsValid = resetPassword.IsStillValidAt(createTime + TimeSpan.FromMinutes(121), ResetPasswordHelpers.ResetPasswordHashExpiryTime); + + // Then + resetIsValid.Should().BeFalse(); + } + + [Test] + public void Set_Password_Is_Valid_4319_Minutes_After_Creation() + { + // Given + var createTime = DateTime.UtcNow; + var resetPassword = Builder.CreateNew() + .With(rp => rp.PasswordResetDateTime = createTime).Build(); + + // When + var resetIsValid = resetPassword.IsStillValidAt(createTime + TimeSpan.FromMinutes(4319), ResetPasswordHelpers.SetPasswordHasExpiryTime); + + // Then + resetIsValid.Should().BeTrue(); + } + + [Test] + public void Set_Password_Is_Invalid_121_Minutes_After_Creation() + { + // Given + var createTime = DateTime.UtcNow; + var resetPassword = Builder.CreateNew() + .With(rp => rp.PasswordResetDateTime = createTime).Build(); + + // When + var resetIsValid = resetPassword.IsStillValidAt(createTime + TimeSpan.FromMinutes(4321), ResetPasswordHelpers.SetPasswordHasExpiryTime); // Then resetIsValid.Should().BeFalse(); diff --git a/DigitalLearningSolutions.Data.Tests/Services/PasswordResetServiceTests.cs b/DigitalLearningSolutions.Data.Tests/Services/PasswordResetServiceTests.cs index 717f4e24da..71bf7c0a3f 100644 --- a/DigitalLearningSolutions.Data.Tests/Services/PasswordResetServiceTests.cs +++ b/DigitalLearningSolutions.Data.Tests/Services/PasswordResetServiceTests.cs @@ -12,28 +12,24 @@ using DigitalLearningSolutions.Data.Models.Email; using DigitalLearningSolutions.Data.Models.User; using DigitalLearningSolutions.Data.Services; - using DigitalLearningSolutions.Data.Tests.Helpers; using DigitalLearningSolutions.Data.Tests.TestHelpers; using FakeItEasy; using FizzWare.NBuilder; using FluentAssertions; - using Microsoft.Extensions.Logging; using NUnit.Framework; public class PasswordResetServiceTests { - private IPasswordResetDataService passwordResetDataService = null!; + private IClockService clockService = null!; private IEmailService emailService = null!; - private ILogger logger = null!; + private IPasswordResetDataService passwordResetDataService = null!; private PasswordResetService passwordResetService = null!; private IUserService userService = null!; - private IClockService clockService = null!; [SetUp] public void SetUp() { userService = A.Fake(); - logger = A.Fake>(); emailService = A.Fake(); clockService = A.Fake(); passwordResetDataService = A.Fake(); @@ -43,14 +39,15 @@ public void SetUp() ( UserTestHelper.GetDefaultAdminUser(), new List { UserTestHelper.GetDefaultDelegateUser() } - )); + ) + ); passwordResetService = new PasswordResetService( userService, passwordResetDataService, - logger, emailService, - clockService); + clockService + ); } [Test] @@ -61,7 +58,8 @@ public void Trying_get_null_user_should_throw_an_exception() // Then Assert.Throws( - () => passwordResetService.GenerateAndSendPasswordResetLink("recipient@example.com", "example.com")); + () => passwordResetService.GenerateAndSendPasswordResetLink("recipient@example.com", "example.com") + ); } [Test] @@ -88,10 +86,11 @@ public void Trying_to_send_password_reset_sends_email() e.To[0] == emailAddress && e.Cc.IsNullOrEmpty() && e.Bcc.IsNullOrEmpty() && - e.Subject == "Digital Learning Solutions Tracking System Password Reset")) + e.Subject == "Digital Learning Solutions Tracking System Password Reset" + ) + ) ) .MustHaveHappened(); - ; } [Test] @@ -108,13 +107,16 @@ public async Task Reset_password_is_discounted_if_expired() A.CallTo( () => passwordResetDataService.FindMatchingResetPasswordEntitiesWithUserDetailsAsync( emailAddress, - hash)) + hash + ) + ) .Returns(Task.FromResult(new[] { resetPasswordWithUserDetails }.ToList())); GivenCurrentTimeIs(createTime + TimeSpan.FromMinutes(125)); // When - var hashIsValid = await passwordResetService.EmailAndResetPasswordHashAreValidAsync(emailAddress, hash); + var hashIsValid = + await passwordResetService.EmailAndResetPasswordHashAreValidAsync(emailAddress, hash, false); // Then hashIsValid.Should().BeFalse(); @@ -139,14 +141,16 @@ public async Task User_references_are_correctly_calculated() A.CallTo( () => passwordResetDataService.FindMatchingResetPasswordEntitiesWithUserDetailsAsync( emailAddress, - resetHash)) + resetHash + ) + ) .Returns(Task.FromResult(resetPasswords)); GivenCurrentTimeIs(createTime + TimeSpan.FromMinutes(2)); // When var hashIsValid = - await passwordResetService.EmailAndResetPasswordHashAreValidAsync(emailAddress, resetHash); + await passwordResetService.EmailAndResetPasswordHashAreValidAsync(emailAddress, resetHash, false); // Then hashIsValid.Should().BeTrue(); @@ -173,6 +177,49 @@ public async Task Removes_reset_password_for_exact_users_matching_email() .WhenArgumentsMatch(args => args.Get(0) != 1 && args.Get(0) != 4).MustNotHaveHappened(); } + [Test] + public void Trying_get_null_user_when_generating_welcome_email_should_throw_an_exception() + { + // Given + A.CallTo(() => userService.GetUsersByEmailAddress(A._)).Returns((null, new List())); + + // Then + Assert.Throws( + () => passwordResetService.GenerateAndSendDelegateWelcomeEmail("recipient@example.com", "example.com") + ); + } + + [Test] + public void Trying_to_send_password_reset_when_generating_welcome_email_sends_email() + { + // Given + var emailAddress = "recipient@example.com"; + var delegateUser = Builder.CreateNew() + .With(user => user.EmailAddress = emailAddress) + .Build(); + + A.CallTo(() => userService.GetUsersByEmailAddress(emailAddress)) + .Returns((null, new List{ delegateUser })); + + // When + passwordResetService.GenerateAndSendDelegateWelcomeEmail(emailAddress, "example.com"); + + // Then + A.CallTo( + () => + emailService.SendEmail( + A.That.Matches( + e => + e.To[0] == emailAddress && + e.Cc.IsNullOrEmpty() && + e.Bcc.IsNullOrEmpty() && + e.Subject == "Welcome to Digital Learning Solutions - Verify your Registration" + ) + ) + ) + .MustHaveHappened(); + } + private void GivenCurrentTimeIs(DateTime validationTime) { A.CallTo(() => clockService.UtcNow).Returns(validationTime); diff --git a/DigitalLearningSolutions.Data.Tests/TestHelpers/EmailTestHelper.cs b/DigitalLearningSolutions.Data.Tests/TestHelpers/EmailTestHelper.cs index 6f399bf455..1b5165ad97 100644 --- a/DigitalLearningSolutions.Data.Tests/TestHelpers/EmailTestHelper.cs +++ b/DigitalLearningSolutions.Data.Tests/TestHelpers/EmailTestHelper.cs @@ -5,7 +5,7 @@ public static class EmailTestHelper { - public const string DefaultHtmlBody = "" + + public const string DefaultHtmlBody = "" + "

Test Body

\r\n" + ""; diff --git a/DigitalLearningSolutions.Data/Helpers/ResetPasswordHelpers.cs b/DigitalLearningSolutions.Data/Helpers/ResetPasswordHelpers.cs index 3a12d99d97..b009e49c55 100644 --- a/DigitalLearningSolutions.Data/Helpers/ResetPasswordHelpers.cs +++ b/DigitalLearningSolutions.Data/Helpers/ResetPasswordHelpers.cs @@ -8,11 +8,12 @@ namespace DigitalLearningSolutions.Data.Helpers public static class ResetPasswordHelpers { - private static readonly TimeSpan ResetPasswordHashExpiryTime = TimeSpan.FromHours(2); + public static readonly TimeSpan ResetPasswordHashExpiryTime = TimeSpan.FromHours(2); + public static readonly TimeSpan SetPasswordHasExpiryTime = TimeSpan.FromDays(3); - public static bool IsStillValidAt(this ResetPassword passwordReset, DateTime dateTime) + public static bool IsStillValidAt(this ResetPassword passwordReset, DateTime dateTime, TimeSpan expiryTime) { - return passwordReset.PasswordResetDateTime + ResetPasswordHashExpiryTime > dateTime; + return passwordReset.PasswordResetDateTime + expiryTime > dateTime; } public static IEnumerable GetDistinctResetPasswordIds(this (AdminUser?, List) userAccounts) diff --git a/DigitalLearningSolutions.Data/Services/DelegateApprovalsService.cs b/DigitalLearningSolutions.Data/Services/DelegateApprovalsService.cs index 33523f23a1..ab473af169 100644 --- a/DigitalLearningSolutions.Data/Services/DelegateApprovalsService.cs +++ b/DigitalLearningSolutions.Data/Services/DelegateApprovalsService.cs @@ -183,7 +183,7 @@ private static Email GenerateDelegateApprovalEmail( You can now log in to Digital Learning Solutions using your e-mail address or your Delegate ID number ""{candidateNumber}"" and the password you chose during registration, using the URL: {loginUrl} . For more assistance in accessing the materials, please contact your Digital Learning Solutions centre. {(centreInformationUrl == null ? "" : $@"View centre contact information: {centreInformationUrl}")}", - HtmlBody = $@" + HtmlBody = $@"

Your Digital Learning Solutions registration has been approved by your centre administrator.

You can now log in to Digital Learning Solutions using your e-mail address or your Delegate ID number ""{candidateNumber}"" and the password you chose during registration.

For more assistance in accessing the materials, please contact your Digital Learning Solutions centre.

@@ -212,7 +212,7 @@ Your Digital Learning Solutions (DLS) registration at the centre {centreName} ha -Your DLS centre chooses to manage delegate registration internally -You have accidentally chosen the wrong centre during the registration process. If you need access to the DLS platform, please use the Find Your Centre page to locate your local DLS centre and use the contact details provided there to ask for help with registration. The Find Your Centre page can be found at this URL: {findCentreUrl}", - HtmlBody = $@" + HtmlBody = $@"

Dear {delegateName},

Your Digital Learning Solutions (DLS) registration at the centre {centreName} has been rejected by an administrator.

There are several reasons that this may have happened including:

diff --git a/DigitalLearningSolutions.Data/Services/NotificationService.cs b/DigitalLearningSolutions.Data/Services/NotificationService.cs index df29e61e20..16c7c37aac 100644 --- a/DigitalLearningSolutions.Data/Services/NotificationService.cs +++ b/DigitalLearningSolutions.Data/Services/NotificationService.cs @@ -48,7 +48,7 @@ public void SendUnlockRequest(int progressId) Digital Learning Solutions Delegate, {unlockData?.DelegateName}, has requested that you unlock their progress for the course {unlockData?.CourseName}. They have reached the maximum number of assessment attempt allowed without passing. To review and unlock their progress, visit the this url: ${unlockUrl.Uri}.", - HtmlBody = $@" + HtmlBody = $@"

Dear {unlockData?.ContactForename}

Digital Learning Solutions Delegate, {unlockData?.DelegateName}, has requested that you unlock their progress for the course {unlockData?.CourseName}

They have reached the maximum number of assessment attempt allowed without passing.

To review and unlock their progress, click here.

diff --git a/DigitalLearningSolutions.Data/Services/PasswordResetService.cs b/DigitalLearningSolutions.Data/Services/PasswordResetService.cs index 6ee37cbbc1..9b3a5b939a 100644 --- a/DigitalLearningSolutions.Data/Services/PasswordResetService.cs +++ b/DigitalLearningSolutions.Data/Services/PasswordResetService.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; + using System.Transactions; using DigitalLearningSolutions.Data.DataServices; using DigitalLearningSolutions.Data.Enums; using DigitalLearningSolutions.Data.Exceptions; @@ -11,51 +12,62 @@ using DigitalLearningSolutions.Data.Models.Auth; using DigitalLearningSolutions.Data.Models.Email; using DigitalLearningSolutions.Data.Models.User; - using Microsoft.Extensions.Logging; using MimeKit; public interface IPasswordResetService { - Task EmailAndResetPasswordHashAreValidAsync(string emailAddress, string resetHash); + Task EmailAndResetPasswordHashAreValidAsync( + string emailAddress, + string resetHash, + bool isSetPassword + ); + void GenerateAndSendPasswordResetLink(string emailAddress, string baseUrl); Task InvalidateResetPasswordForEmailAsync(string email); - public string GenerateResetPasswordHash(User user); + void GenerateAndSendDelegateWelcomeEmail(string emailAddress, string baseUrl); } public class PasswordResetService : IPasswordResetService { - private readonly IPasswordResetDataService passwordResetDataService; - private readonly IEmailService emailService; private readonly IClockService clockService; - private readonly ILogger logger; + private readonly IEmailService emailService; + private readonly IPasswordResetDataService passwordResetDataService; private readonly IUserService userService; public PasswordResetService( IUserService userService, IPasswordResetDataService passwordResetDataService, - ILogger logger, IEmailService emailService, - IClockService clockService) + IClockService clockService + ) { this.userService = userService; this.passwordResetDataService = passwordResetDataService; - this.logger = logger; this.emailService = emailService; this.clockService = clockService; } public async Task EmailAndResetPasswordHashAreValidAsync( string emailAddress, - string resetHash) + string resetHash, + bool isSetPassword + ) { var matchingResetPasswordEntities = await passwordResetDataService.FindMatchingResetPasswordEntitiesWithUserDetailsAsync( emailAddress, - resetHash); + resetHash + ); return matchingResetPasswordEntities.Any( - resetPassword => resetPassword.IsStillValidAt(clockService.UtcNow)); + resetPassword => resetPassword.IsStillValidAt( + clockService.UtcNow, + isSetPassword + ? ResetPasswordHelpers.SetPasswordHasExpiryTime + : ResetPasswordHelpers.ResetPasswordHashExpiryTime + ) + ); } public void GenerateAndSendPasswordResetLink(string emailAddress, string baseUrl) @@ -63,13 +75,15 @@ public void GenerateAndSendPasswordResetLink(string emailAddress, string baseUrl (User? user, List delegateUsers) = userService.GetUsersByEmailAddress(emailAddress); user ??= delegateUsers.FirstOrDefault() ?? throw new UserAccountNotFoundException( - "No user account could be found with the specified email address"); + "No user account could be found with the specified email address" + ); string resetPasswordHash = GenerateResetPasswordHash(user); var resetPasswordEmail = GeneratePasswordResetEmail( emailAddress, resetPasswordHash, user.FirstName, - baseUrl); + baseUrl + ); emailService.SendEmail(resetPasswordEmail); } @@ -83,7 +97,28 @@ public async Task InvalidateResetPasswordForEmailAsync(string email) } } - public string GenerateResetPasswordHash(User user) + public void GenerateAndSendDelegateWelcomeEmail(string emailAddress, string baseUrl) + { + (_, List delegateUsers) = userService.GetUsersByEmailAddress(emailAddress); + var delegateUser = delegateUsers.FirstOrDefault() ?? + throw new UserAccountNotFoundException( + "No user account could be found with the specified email address" + ); + + string setPasswordHash = GenerateResetPasswordHash(delegateUser); + var welcomeEmail = GenerateWelcomeEmail( + emailAddress, + setPasswordHash, + baseUrl, + delegateUser.FirstName, + delegateUser.LastName, + delegateUser.CentreName, + delegateUser.CandidateNumber + ); + emailService.SendEmail(welcomeEmail); + } + + private string GenerateResetPasswordHash(User user) { string hash = Guid.NewGuid().ToString(); @@ -91,7 +126,8 @@ public string GenerateResetPasswordHash(User user) clockService.UtcNow, hash, user.Id, - user is DelegateUser ? UserType.DelegateUser : UserType.AdminUser); + user is DelegateUser ? UserType.DelegateUser : UserType.AdminUser + ); passwordResetDataService.CreatePasswordReset(resetPasswordCreateModel); @@ -102,7 +138,8 @@ private static Email GeneratePasswordResetEmail( string emailAddress, string resetHash, string? firstName, - string baseUrl) + string baseUrl + ) { UriBuilder resetPasswordUrl = new UriBuilder(baseUrl); if (!resetPasswordUrl.Path.EndsWith('/')) @@ -124,7 +161,7 @@ private static Email GeneratePasswordResetEmail( To reset your password please follow this link: {resetPasswordUrl.Uri} Note that this link can only be used once and it will expire in two hours. Please don’t reply to this email as it has been automatically generated.", - HtmlBody = $@" + HtmlBody = $@"

Dear {nameToUse},

A request has been made to reset the password for your Digital Learning Solutions account.

To reset your password please follow this link: {resetPasswordUrl.Uri}

@@ -135,5 +172,49 @@ Note that this link can only be used once and it will expire in two hours. return new Email(emailSubject, body, emailAddress); } + + private Email GenerateWelcomeEmail( + string emailAddress, + string setPasswordHash, + string baseUrl, + string? firstName, + string lastName, + string centreName, + string candidateNumber + ) + { + UriBuilder setPasswordUrl = new UriBuilder(baseUrl); + if (!setPasswordUrl.Path.EndsWith('/')) + { + setPasswordUrl.Path += '/'; + } + + setPasswordUrl.Path += "SetPassword"; + setPasswordUrl.Query = $"code={setPasswordHash}&email={emailAddress}"; + + const string emailSubject = "Welcome to Digital Learning Solutions - Verify your Registration"; + + var nameToUse = firstName != null ? $"{firstName} {lastName}" : lastName; + + BodyBuilder body = new BodyBuilder + { + TextBody = $@"Dear {nameToUse}, + An administrator has registered your details to give you access to the Digital Learning Solutions (DLS) platform under the centre {centreName}. + You have been assigned the unique DLS delegate number {candidateNumber}. + To complete your registration and access your Digital Learning Solutions content, please click: {setPasswordUrl.Uri} + Note that this link can only be used once and it will expire in three days. + Please don't reply to this email as it has been automatically generated.", + HtmlBody = $@" +

Dear {nameToUse},

+

An administrator has registered your details to give you access to the Digital Learning Solutions (DLS) platform under the centre {centreName}.

+

You have been assigned the unique DLS delegate number {candidateNumber}.

+

To complete your registration and access your Digital Learning Solutions content, please click this link.

+

Note that this link can only be used once and it will expire in three days.

+

Please don't reply to this email as it has been automatically generated.

+ " + }; + + return new Email(emailSubject, body, emailAddress); + } } } diff --git a/DigitalLearningSolutions.Data/Services/RegistrationService.cs b/DigitalLearningSolutions.Data/Services/RegistrationService.cs index e22324e2fe..c87c86431a 100644 --- a/DigitalLearningSolutions.Data/Services/RegistrationService.cs +++ b/DigitalLearningSolutions.Data/Services/RegistrationService.cs @@ -6,7 +6,6 @@ namespace DigitalLearningSolutions.Data.Services using DigitalLearningSolutions.Data.DataServices; using DigitalLearningSolutions.Data.Models.Email; using DigitalLearningSolutions.Data.Models.Register; - using DigitalLearningSolutions.Data.Models.User; using Microsoft.Extensions.Configuration; using MimeKit; @@ -21,8 +20,6 @@ bool refactoredTrackingSystemEnabled string RegisterDelegateByCentre(DelegateRegistrationModel delegateRegistrationModel); void RegisterCentreManager(RegistrationModel registrationModel); - - void GenerateAndSendDelegateWelcomeEmail(DelegateUserCard delegateUser); } public class RegistrationService : IRegistrationService @@ -121,24 +118,6 @@ public void RegisterCentreManager(RegistrationModel registrationModel) transaction.Complete(); } - public void GenerateAndSendDelegateWelcomeEmail(DelegateUserCard delegateUser) - { - using var transaction = new TransactionScope(); - - string setPasswordHash = passwordResetService.GenerateResetPasswordHash(delegateUser); - var welcomeEmail = GenerateWelcomeEmail( - delegateUser.EmailAddress!.Trim(), - delegateUser.FirstName!, - delegateUser.LastName, - delegateUser.CentreName, - delegateUser.CandidateNumber, - setPasswordHash - ); - emailService.SendEmail(welcomeEmail); - - transaction.Complete(); - } - private void CreateDelegateAccountForAdmin(RegistrationModel registrationModel) { var delegateRegistrationModel = new DelegateRegistrationModel( @@ -183,7 +162,7 @@ bool refactoredTrackingSystemEnabled A learner, {learnerFirstName} {learnerLastName}, has registered against your Digital Learning Solutions centre and requires approval before they can access courses. To approve or reject their registration please follow this link: {approvalUrl} Please don't reply to this email as it has been automatically generated.", - HtmlBody = $@" + HtmlBody = $@"

Dear {firstName},

A learner, {learnerFirstName} {learnerLastName}, has registered against your Digital Learning Solutions centre and requires approval before they can access courses.

To approve or reject their registration please follow this link: {approvalUrl}

@@ -193,38 +172,5 @@ bool refactoredTrackingSystemEnabled return new Email(emailSubject, body, emailAddress); } - - private Email GenerateWelcomeEmail( - string emailAddress, - string firstName, - string lastName, - string centreName, - string candidateNumber, - string setPasswordHash - ) - { - const string emailSubject = "Welcome to Digital Learning Solutions - Verify your Registration"; - var setPasswordUrl = $"{config["AppRootPath"]}/SetPassword?code={setPasswordHash}&email={emailAddress}"; - - BodyBuilder body = new BodyBuilder - { - TextBody = $@"Dear {firstName} {lastName}, - An administrator has registered your details to give you access to the Digital Learning Solutions (DLS) platform under the centre {centreName}. - You have been assigned the unique DLS delegate number {candidateNumber}. - To complete your registration and access your Digital Learning Solutions content, please follow this link:{setPasswordUrl} - Note that this link can only be used once. - Please don't reply to this email as it has been automatically generated.", - HtmlBody = $@" -

Dear {firstName},

-

An administrator has registered your details to give you access to the Digital Learning Solutions (DLS) platform under the centre {centreName}.

-

You have been assigned the unique DLS delegate number {candidateNumber}.

-

To complete your registration and access your Digital Learning Solutions content, please follow this link: {setPasswordUrl}

-

Note that this link can only be used once.

-

Please don't reply to this email as it has been automatically generated.

- " - }; - - return new Email(emailSubject, body, emailAddress); - } } } diff --git a/DigitalLearningSolutions.Web.Tests/Controllers/ResetPasswordControllerTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/ResetPasswordControllerTests.cs index 995f5e45e7..e7496185b5 100644 --- a/DigitalLearningSolutions.Web.Tests/Controllers/ResetPasswordControllerTests.cs +++ b/DigitalLearningSolutions.Web.Tests/Controllers/ResetPasswordControllerTests.cs @@ -14,10 +14,10 @@ namespace DigitalLearningSolutions.Web.Tests.Controllers public class ResetPasswordControllerTests { - private ResetPasswordController authenticatedController; - private ResetPasswordController unauthenticatedController; - private IPasswordResetService passwordResetService; - private IPasswordService passwordService; + private ResetPasswordController authenticatedController = null!; + private ResetPasswordController unauthenticatedController = null!; + private IPasswordResetService passwordResetService = null!; + private IPasswordService passwordService = null!; [SetUp] public void SetUp() @@ -49,7 +49,7 @@ public async Task Index_should_redirect_to_homepage_if_user_is_authenticated() public async Task Index_should_render_if_user_is_unauthenticated_and_query_params_are_valid() { // Given - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "code")) + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "code", false)) .Returns(Task.FromResult(true)); // When @@ -63,7 +63,7 @@ public async Task Index_should_render_if_user_is_unauthenticated_and_query_param public async Task Index_should_redirect_to_error_page_if_user_is_unauthenticated_and_query_params_are_invalid() { // Given - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "code")) + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "code", false)) .Returns(Task.FromResult(false)); // When @@ -97,7 +97,7 @@ public async Task Index_should_redirect_to_login_page_if_user_is_unauthenticated public async Task Index_should_set_email_and_hash_in_temp_data_if_valid() { // Given - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash")) + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", false)) .Returns(true); // When @@ -112,7 +112,7 @@ public async Task Index_should_set_email_and_hash_in_temp_data_if_valid() public async Task Post_to_index_should_invalidate_reset_hash_if_model_and_hash_valid() { // Given - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash")).Returns(true); + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", false)).Returns(true); unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); // When @@ -129,7 +129,7 @@ public async Task Post_to_index_should_update_password_if_model_and_hash_valid() { // Given unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash")).Returns(true); + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", false)).Returns(true); // When await unauthenticatedController.Index( @@ -145,7 +145,7 @@ public async Task Post_to_index_should_return_success_page_if_model_and_hash_val { // Given unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash")).Returns(true); + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", false)).Returns(true); // When var result = await unauthenticatedController.Index( @@ -161,7 +161,7 @@ public async Task Post_to_index_should_clear_temp_data_if_model_and_hash_valid() // Given unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); unauthenticatedController.TempData.Set("some string"); - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash")).Returns(true); + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", false)).Returns(true); // When await unauthenticatedController.Index( @@ -178,7 +178,7 @@ public async Task Post_to_index_should_clear_temp_data_if_hash_invalid() // Given unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); unauthenticatedController.TempData.Set("some string"); - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash")).Returns(false); + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", false)).Returns(false); // When await unauthenticatedController.Index( @@ -194,7 +194,7 @@ public async Task Post_to_index_should_preserve_temp_data_if_model_invalid() { // Given unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash")).Returns(true); + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", false)).Returns(true); unauthenticatedController.ModelState.AddModelError("model", "Invalid for testing"); // When @@ -212,7 +212,7 @@ public async Task Post_to_index_should_return_form_if_model_state_invalid() // Given unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); unauthenticatedController.ModelState.AddModelError("Testings", "errors for testing"); - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash")).Returns(true); + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", false)).Returns(true); // When var result = await unauthenticatedController.Index( @@ -226,7 +226,7 @@ public async Task Post_to_index_should_return_form_if_model_state_invalid() public async Task Post_to_index_should_redirect_to_Error_if_reset_password_invalid() { // Given - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash")).Returns(false); + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", false)).Returns(false); unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); // When diff --git a/DigitalLearningSolutions.Web.Tests/Controllers/SetPasswordControllerTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/SetPasswordControllerTests.cs new file mode 100644 index 0000000000..25a6579c68 --- /dev/null +++ b/DigitalLearningSolutions.Web.Tests/Controllers/SetPasswordControllerTests.cs @@ -0,0 +1,240 @@ +namespace DigitalLearningSolutions.Web.Tests.Controllers +{ + using System.Threading.Tasks; + using DigitalLearningSolutions.Data.Services; + using DigitalLearningSolutions.Web.Controllers; + using DigitalLearningSolutions.Web.Extensions; + using DigitalLearningSolutions.Web.Models; + using DigitalLearningSolutions.Web.Tests.ControllerHelpers; + using DigitalLearningSolutions.Web.ViewModels.Common; + using FakeItEasy; + using FluentAssertions; + using FluentAssertions.AspNetCore.Mvc; + using NUnit.Framework; + + public class SetPasswordControllerTests + { + private SetPasswordController authenticatedController = null!; + private SetPasswordController unauthenticatedController = null!; + private IPasswordResetService passwordResetService = null!; + private IPasswordService passwordService = null!; + + [SetUp] + public void SetUp() + { + passwordResetService = A.Fake(); + passwordService = A.Fake(); + + unauthenticatedController = new SetPasswordController(passwordResetService, passwordService) + .WithDefaultContext() + .WithMockTempData() + .WithMockUser(false); + authenticatedController = new SetPasswordController(passwordResetService, passwordService) + .WithDefaultContext() + .WithMockTempData() + .WithMockUser(true); + } + + [Test] + public async Task Index_should_redirect_to_homepage_if_user_is_authenticated() + { + // When + var result = await authenticatedController.Index("email", "code"); + + // Then + result.Should().BeRedirectToActionResult().WithControllerName("Home").WithActionName("Index"); + } + + [Test] + public async Task Index_should_render_if_user_is_unauthenticated_and_query_params_are_valid() + { + // Given + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "code", true)) + .Returns(Task.FromResult(true)); + + // When + var result = await unauthenticatedController.Index("email", "code"); + + // Then + result.Should().BeViewResult().WithDefaultViewName(); + } + + [Test] + public async Task Index_should_redirect_to_error_page_if_user_is_unauthenticated_and_query_params_are_invalid() + { + // Given + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "code", true)) + .Returns(Task.FromResult(false)); + + // When + var result = await unauthenticatedController.Index("email", "code"); + + // Then + result.Should().BeRedirectToActionResult().WithActionName("Error"); + } + + [Test] + public async Task Index_should_redirect_to_login_page_if_user_is_unauthenticated_and_email_is_missing() + { + // When + var result = await unauthenticatedController.Index(null, "code"); + + // Then + result.Should().BeRedirectToActionResult().WithControllerName("Login").WithActionName("Index"); + } + + [Test] + public async Task Index_should_redirect_to_login_page_if_user_is_unauthenticated_and_code_is_missing() + { + // When + var result = await unauthenticatedController.Index("email", null); + + // Then + result.Should().BeRedirectToActionResult().WithControllerName("Login").WithActionName("Index"); + } + + [Test] + public async Task Index_should_set_email_and_hash_in_temp_data_if_valid() + { + // Given + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", true)) + .Returns(true); + + // When + await unauthenticatedController.Index("email", "hash"); + + // Then + unauthenticatedController.TempData.Peek().Should().BeEquivalentTo( + new ResetPasswordData("email", "hash")); + } + + [Test] + public async Task Post_to_index_should_invalidate_reset_hash_if_model_and_hash_valid() + { + // Given + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", true)).Returns(true); + unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); + + // When + await unauthenticatedController.Index( + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + + // Then + A.CallTo(() => passwordResetService.InvalidateResetPasswordForEmailAsync("email")) + .MustHaveHappened(1, Times.Exactly); + } + + [Test] + public async Task Post_to_index_should_update_password_if_model_and_hash_valid() + { + // Given + unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", true)).Returns(true); + + // When + await unauthenticatedController.Index( + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + + // Then + A.CallTo(() => passwordService.ChangePasswordAsync("email", "testPass-9")) + .MustHaveHappened(1, Times.Exactly); + } + + [Test] + public async Task Post_to_index_should_return_success_page_if_model_and_hash_valid() + { + // Given + unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", true)).Returns(true); + + // When + var result = await unauthenticatedController.Index( + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + + // Then + result.Should().BeViewResult().WithViewName("Success"); + } + + [Test] + public async Task Post_to_index_should_clear_temp_data_if_model_and_hash_valid() + { + // Given + unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); + unauthenticatedController.TempData.Set("some string"); + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", true)).Returns(true); + + // When + await unauthenticatedController.Index( + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + + // Then + unauthenticatedController.TempData.Peek().Should().BeNull(); + unauthenticatedController.TempData.Peek().Should().BeNull(); + } + + [Test] + public async Task Post_to_index_should_clear_temp_data_if_hash_invalid() + { + // Given + unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); + unauthenticatedController.TempData.Set("some string"); + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", true)).Returns(false); + + // When + await unauthenticatedController.Index( + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + + // Then + unauthenticatedController.TempData.Peek().Should().BeNull(); + unauthenticatedController.TempData.Peek().Should().BeNull(); + } + + [Test] + public async Task Post_to_index_should_preserve_temp_data_if_model_invalid() + { + // Given + unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", true)).Returns(true); + unauthenticatedController.ModelState.AddModelError("model", "Invalid for testing"); + + // When + await unauthenticatedController.Index( + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + + // Then + unauthenticatedController.TempData.Peek().Should() + .BeEquivalentTo(new ResetPasswordData("email", "hash")); + } + + [Test] + public async Task Post_to_index_should_return_form_if_model_state_invalid() + { + // Given + unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); + unauthenticatedController.ModelState.AddModelError("Testings", "errors for testing"); + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", true)).Returns(true); + + // When + var result = await unauthenticatedController.Index( + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + + // Then + result.Should().BeViewResult().WithDefaultViewName(); + } + + [Test] + public async Task Post_to_index_should_redirect_to_Error_if_reset_password_invalid() + { + // Given + A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", true)).Returns(false); + unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); + + // When + var result = await unauthenticatedController.Index( + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + + // Then + result.Should().BeRedirectToActionResult().WithActionName("Error").WithControllerName(null); + } + } +} diff --git a/DigitalLearningSolutions.Web/Controllers/ResetPasswordController.cs b/DigitalLearningSolutions.Web/Controllers/ResetPasswordController.cs index 4f0067a3db..66a6b3889d 100644 --- a/DigitalLearningSolutions.Web/Controllers/ResetPasswordController.cs +++ b/DigitalLearningSolutions.Web/Controllers/ResetPasswordController.cs @@ -1,10 +1,71 @@ namespace DigitalLearningSolutions.Web.Controllers { + using System.Threading.Tasks; using DigitalLearningSolutions.Data.Services; + using DigitalLearningSolutions.Web.Extensions; + using DigitalLearningSolutions.Web.Models; + using DigitalLearningSolutions.Web.ServiceFilter; + using DigitalLearningSolutions.Web.ViewModels.Common; + using Microsoft.AspNetCore.Mvc; public class ResetPasswordController : SetPasswordControllerBase { public ResetPasswordController(IPasswordResetService passwordResetService, IPasswordService passwordService) : base(passwordResetService, passwordService) { } + + [HttpGet] + public async Task Index(string email, string code) + { + if (User.Identity.IsAuthenticated) + { + return RedirectToAction("Index", "Home"); + } + + if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(code)) + { + return RedirectToAction("Index", "Login"); + } + + var hashIsValid = await IsSetPasswordLinkValid(email, code); + + TempData.Set(new ResetPasswordData(email, code)); + + if (!hashIsValid) + { + return RedirectToAction("Error"); + } + + return View(new ConfirmPasswordViewModel()); + } + + [HttpPost] + [ServiceFilter(typeof(RedirectEmptySessionData))] + public async Task Index(ConfirmPasswordViewModel viewModel) + { + var resetPasswordData = TempData.Peek()!; + + var hashIsValid = await IsSetPasswordLinkValid(resetPasswordData); + + if (!hashIsValid) + { + TempData.Clear(); + return RedirectToAction("Error"); + } + + if (!ModelState.IsValid) + { + return View(viewModel); + } + + await ChangePassword(viewModel, resetPasswordData); + + return View("Success"); + } + + [HttpGet] + public IActionResult Error() + { + return View(); + } } } diff --git a/DigitalLearningSolutions.Web/Controllers/SetPasswordController.cs b/DigitalLearningSolutions.Web/Controllers/SetPasswordController.cs index ac8d74dcda..0b7f4a89f0 100644 --- a/DigitalLearningSolutions.Web/Controllers/SetPasswordController.cs +++ b/DigitalLearningSolutions.Web/Controllers/SetPasswordController.cs @@ -1,10 +1,73 @@ namespace DigitalLearningSolutions.Web.Controllers { + using System.Threading.Tasks; using DigitalLearningSolutions.Data.Services; + using DigitalLearningSolutions.Web.Extensions; + using DigitalLearningSolutions.Web.Models; + using DigitalLearningSolutions.Web.ServiceFilter; + using DigitalLearningSolutions.Web.ViewModels.Common; + using Microsoft.AspNetCore.Mvc; public class SetPasswordController : SetPasswordControllerBase { public SetPasswordController(IPasswordResetService passwordResetService, IPasswordService passwordService) : base(passwordResetService, passwordService) { } + + [HttpGet] + public async Task Index(string email, string code) + { + if (User.Identity.IsAuthenticated) + { + return RedirectToAction("Index", "Home"); + } + + if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(code)) + { + return RedirectToAction("Index", "Login"); + } + + var hashIsValid = await IsSetPasswordLinkValid(email, code, true); + + TempData.Set(new ResetPasswordData(email, code)); + + if (!hashIsValid) + { + return RedirectToAction("Error"); + } + + return View(new ConfirmPasswordViewModel()); + } + + [HttpPost] + [ServiceFilter(typeof(RedirectEmptySessionData))] + public async Task Index(ConfirmPasswordViewModel viewModel) + { + var resetPasswordData = TempData.Peek()!; + + var hashIsValid = await IsSetPasswordLinkValid(resetPasswordData, true); + + if (!hashIsValid) + { + TempData.Clear(); + return RedirectToAction("Error"); + } + + if (!ModelState.IsValid) + { + return View(viewModel); + } + + await ChangePassword(viewModel, resetPasswordData); + + return View("Success"); + } + + + + [HttpGet] + public IActionResult Error() + { + return View(); + } } } diff --git a/DigitalLearningSolutions.Web/Controllers/SetPasswordControllerBase.cs b/DigitalLearningSolutions.Web/Controllers/SetPasswordControllerBase.cs index aa50db4513..a851f9ab91 100644 --- a/DigitalLearningSolutions.Web/Controllers/SetPasswordControllerBase.cs +++ b/DigitalLearningSolutions.Web/Controllers/SetPasswordControllerBase.cs @@ -2,81 +2,55 @@ namespace DigitalLearningSolutions.Web.Controllers { using System.Threading.Tasks; using DigitalLearningSolutions.Data.Services; - using DigitalLearningSolutions.Web.Extensions; using DigitalLearningSolutions.Web.Models; - using DigitalLearningSolutions.Web.ServiceFilter; using DigitalLearningSolutions.Web.ViewModels.Common; using Microsoft.AspNetCore.Mvc; - public class SetPasswordControllerBase : Controller + public abstract class SetPasswordControllerBase : Controller { - private readonly IPasswordResetService passwordResetService; - private readonly IPasswordService passwordService; + protected readonly IPasswordResetService PasswordResetService; + protected readonly IPasswordService PasswordService; - public SetPasswordControllerBase(IPasswordResetService passwordResetService, IPasswordService passwordService) + protected SetPasswordControllerBase( + IPasswordResetService passwordResetService, + IPasswordService passwordService + ) { - this.passwordResetService = passwordResetService; - this.passwordService = passwordService; + PasswordResetService = passwordResetService; + PasswordService = passwordService; } - [HttpGet] - public async Task Index(string email, string code) + protected async Task IsSetPasswordLinkValid( + string emailAddress, + string resetHash, + bool isSetPassword = false + ) { - if (User.Identity.IsAuthenticated) - { - return RedirectToAction("Index", "Home"); - } - - if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(code)) - { - return RedirectToAction("Index", "Login"); - } - - var hashIsValid = await passwordResetService.EmailAndResetPasswordHashAreValidAsync(email, code); - - TempData.Set(new ResetPasswordData(email, code)); - - if (!hashIsValid) - { - return RedirectToAction("Error"); - } - - return View(new ConfirmPasswordViewModel()); + return await PasswordResetService.EmailAndResetPasswordHashAreValidAsync( + emailAddress, + resetHash, + isSetPassword + ); } - [HttpPost] - [ServiceFilter(typeof(RedirectEmptySessionData))] - public async Task Index(ConfirmPasswordViewModel viewModel) + protected async Task IsSetPasswordLinkValid( + ResetPasswordData resetPasswordData, + bool isSetPassword = false + ) { - var resetPasswordData = TempData.Peek()!; - - var hashIsValid = await passwordResetService.EmailAndResetPasswordHashAreValidAsync( + return await IsSetPasswordLinkValid( resetPasswordData.Email, - resetPasswordData.ResetPasswordHash); - - if (!hashIsValid) - { - TempData.Clear(); - return RedirectToAction("Error"); - } - - if (!ModelState.IsValid) - { - return View(viewModel); - } - - await passwordResetService.InvalidateResetPasswordForEmailAsync(resetPasswordData.Email); - await passwordService.ChangePasswordAsync(resetPasswordData.Email, viewModel.Password!); - - TempData.Clear(); - - return View("Success"); + resetPasswordData.ResetPasswordHash, + isSetPassword + ); } - [HttpGet] - public IActionResult Error() + protected async Task ChangePassword(ConfirmPasswordViewModel viewModel, ResetPasswordData resetPasswordData) { - return View(); + await PasswordResetService.InvalidateResetPasswordForEmailAsync(resetPasswordData.Email); + await PasswordService.ChangePasswordAsync(resetPasswordData.Email, viewModel.Password!); + + TempData.Clear(); } } } diff --git a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/ViewDelegateController.cs b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/ViewDelegateController.cs index 3e426d206c..f04be6d4c0 100644 --- a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/ViewDelegateController.cs +++ b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/ViewDelegateController.cs @@ -18,20 +18,20 @@ public class ViewDelegateController : Controller { private readonly ICourseService courseService; private readonly CustomPromptHelper customPromptHelper; - private readonly IRegistrationService registrationService; + private readonly IPasswordResetService passwordResetService; private readonly IUserDataService userDataService; public ViewDelegateController( IUserDataService userDataService, CustomPromptHelper customPromptHelper, ICourseService courseService, - IRegistrationService registrationService + IPasswordResetService passwordResetService ) { this.userDataService = userDataService; this.customPromptHelper = customPromptHelper; this.courseService = courseService; - this.registrationService = registrationService; + this.passwordResetService = passwordResetService; } public IActionResult Index(int delegateId) @@ -65,19 +65,12 @@ public IActionResult SendWelcomeEmail(int delegateId) return new NotFoundResult(); } - registrationService.GenerateAndSendDelegateWelcomeEmail(delegateUser!); + string baseUrl = ConfigHelper.GetAppConfig()["AppRootPath"]; - TempData.Set(new WelcomeEmailSentViewModel(delegateUser)); + passwordResetService.GenerateAndSendDelegateWelcomeEmail(delegateUser.EmailAddress!, baseUrl); - return RedirectToAction("WelcomeEmailSent", new { delegateId = delegateUser.Id }); - } + var model = new WelcomeEmailSentViewModel(delegateUser); - [HttpGet] - [Route("WelcomeEmailSent")] - [ServiceFilter(typeof(RedirectEmptySessionData))] - public IActionResult WelcomeEmailSent() - { - var model = TempData.Get()!; return View("WelcomeEmailSent", model); } } diff --git a/DigitalLearningSolutions.Web/Views/SetPassword/Error.cshtml b/DigitalLearningSolutions.Web/Views/SetPassword/Error.cshtml index c8369bd2ef..693fa831ce 100644 --- a/DigitalLearningSolutions.Web/Views/SetPassword/Error.cshtml +++ b/DigitalLearningSolutions.Web/Views/SetPassword/Error.cshtml @@ -8,6 +8,6 @@

Something went wrong...

- There was a problem resetting your password. - The set password link is only valid once, and expires after 2 hours. + There was a problem setting your password. + The set password link is only valid once, and expires after 3 days.

From ab71c0e4df55aa70b5bd26841bc1d05a69c1dfbf Mon Sep 17 00:00:00 2001 From: Alex Jackson Date: Tue, 17 Aug 2021 17:11:28 +0100 Subject: [PATCH 8/9] HEEDLS-495 Remove SetPasswordControllerBase and refactor service --- .../Services/PasswordResetServiceTests.cs | 5 +- .../Services/RegistrationServiceTests.cs | 1 - .../Services/PasswordResetService.cs | 71 +++++----- .../Services/RegistrationService.cs | 3 - .../ResetPasswordControllerTests.cs | 128 +++++++++++++++--- .../Controllers/SetPasswordControllerTests.cs | 128 +++++++++++++++--- .../Controllers/ForgotPasswordController.cs | 2 +- .../ResetPasswordController.cs | 35 ++++- .../SetPasswordController.cs | 37 +++-- .../Controllers/SetPasswordControllerBase.cs | 56 -------- .../Delegates/ViewDelegateController.cs | 4 +- .../Helpers/ConfigHelper.cs | 6 + 12 files changed, 310 insertions(+), 166 deletions(-) rename DigitalLearningSolutions.Web/Controllers/{ => SetPassword}/ResetPasswordController.cs (54%) rename DigitalLearningSolutions.Web/Controllers/{ => SetPassword}/SetPasswordController.cs (54%) delete mode 100644 DigitalLearningSolutions.Web/Controllers/SetPasswordControllerBase.cs diff --git a/DigitalLearningSolutions.Data.Tests/Services/PasswordResetServiceTests.cs b/DigitalLearningSolutions.Data.Tests/Services/PasswordResetServiceTests.cs index 71bf7c0a3f..2aebf21f78 100644 --- a/DigitalLearningSolutions.Data.Tests/Services/PasswordResetServiceTests.cs +++ b/DigitalLearningSolutions.Data.Tests/Services/PasswordResetServiceTests.cs @@ -8,6 +8,7 @@ using DigitalLearningSolutions.Data.DataServices; using DigitalLearningSolutions.Data.Enums; using DigitalLearningSolutions.Data.Exceptions; + using DigitalLearningSolutions.Data.Helpers; using DigitalLearningSolutions.Data.Models.Auth; using DigitalLearningSolutions.Data.Models.Email; using DigitalLearningSolutions.Data.Models.User; @@ -116,7 +117,7 @@ public async Task Reset_password_is_discounted_if_expired() // When var hashIsValid = - await passwordResetService.EmailAndResetPasswordHashAreValidAsync(emailAddress, hash, false); + await passwordResetService.EmailAndResetPasswordHashAreValidAsync(emailAddress, hash, ResetPasswordHelpers.ResetPasswordHashExpiryTime); // Then hashIsValid.Should().BeFalse(); @@ -150,7 +151,7 @@ public async Task User_references_are_correctly_calculated() // When var hashIsValid = - await passwordResetService.EmailAndResetPasswordHashAreValidAsync(emailAddress, resetHash, false); + await passwordResetService.EmailAndResetPasswordHashAreValidAsync(emailAddress, resetHash, ResetPasswordHelpers.ResetPasswordHashExpiryTime); // Then hashIsValid.Should().BeTrue(); diff --git a/DigitalLearningSolutions.Data.Tests/Services/RegistrationServiceTests.cs b/DigitalLearningSolutions.Data.Tests/Services/RegistrationServiceTests.cs index 94a5d1a7d5..89843b2b52 100644 --- a/DigitalLearningSolutions.Data.Tests/Services/RegistrationServiceTests.cs +++ b/DigitalLearningSolutions.Data.Tests/Services/RegistrationServiceTests.cs @@ -84,7 +84,6 @@ public void Setup() registrationService = new RegistrationService( registrationDataService, passwordDataService, - passwordResetService, emailService, centresDataService, config diff --git a/DigitalLearningSolutions.Data/Services/PasswordResetService.cs b/DigitalLearningSolutions.Data/Services/PasswordResetService.cs index 9b3a5b939a..9e6d63bb64 100644 --- a/DigitalLearningSolutions.Data/Services/PasswordResetService.cs +++ b/DigitalLearningSolutions.Data/Services/PasswordResetService.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; - using System.Transactions; using DigitalLearningSolutions.Data.DataServices; using DigitalLearningSolutions.Data.Enums; using DigitalLearningSolutions.Data.Exceptions; @@ -19,7 +18,7 @@ public interface IPasswordResetService Task EmailAndResetPasswordHashAreValidAsync( string emailAddress, string resetHash, - bool isSetPassword + TimeSpan expiryTime ); void GenerateAndSendPasswordResetLink(string emailAddress, string baseUrl); @@ -48,28 +47,6 @@ IClockService clockService this.clockService = clockService; } - public async Task EmailAndResetPasswordHashAreValidAsync( - string emailAddress, - string resetHash, - bool isSetPassword - ) - { - var matchingResetPasswordEntities = - await passwordResetDataService.FindMatchingResetPasswordEntitiesWithUserDetailsAsync( - emailAddress, - resetHash - ); - - return matchingResetPasswordEntities.Any( - resetPassword => resetPassword.IsStillValidAt( - clockService.UtcNow, - isSetPassword - ? ResetPasswordHelpers.SetPasswordHasExpiryTime - : ResetPasswordHelpers.ResetPasswordHashExpiryTime - ) - ); - } - public void GenerateAndSendPasswordResetLink(string emailAddress, string baseUrl) { (User? user, List delegateUsers) = userService.GetUsersByEmailAddress(emailAddress); @@ -101,23 +78,40 @@ public void GenerateAndSendDelegateWelcomeEmail(string emailAddress, string base { (_, List delegateUsers) = userService.GetUsersByEmailAddress(emailAddress); var delegateUser = delegateUsers.FirstOrDefault() ?? - throw new UserAccountNotFoundException( - "No user account could be found with the specified email address" - ); + throw new UserAccountNotFoundException( + "No user account could be found with the specified email address" + ); string setPasswordHash = GenerateResetPasswordHash(delegateUser); var welcomeEmail = GenerateWelcomeEmail( emailAddress, setPasswordHash, baseUrl, - delegateUser.FirstName, - delegateUser.LastName, - delegateUser.CentreName, - delegateUser.CandidateNumber + delegateUser ); emailService.SendEmail(welcomeEmail); } + public async Task EmailAndResetPasswordHashAreValidAsync( + string emailAddress, + string resetHash, + TimeSpan expiryTime + ) + { + var matchingResetPasswordEntities = + await passwordResetDataService.FindMatchingResetPasswordEntitiesWithUserDetailsAsync( + emailAddress, + resetHash + ); + + return matchingResetPasswordEntities.Any( + resetPassword => resetPassword.IsStillValidAt( + clockService.UtcNow, + expiryTime + ) + ); + } + private string GenerateResetPasswordHash(User user) { string hash = Guid.NewGuid().ToString(); @@ -177,10 +171,7 @@ private Email GenerateWelcomeEmail( string emailAddress, string setPasswordHash, string baseUrl, - string? firstName, - string lastName, - string centreName, - string candidateNumber + DelegateUser delegateUser ) { UriBuilder setPasswordUrl = new UriBuilder(baseUrl); @@ -194,20 +185,20 @@ string candidateNumber const string emailSubject = "Welcome to Digital Learning Solutions - Verify your Registration"; - var nameToUse = firstName != null ? $"{firstName} {lastName}" : lastName; + var nameToUse = delegateUser.FirstName != null ? $"{delegateUser.FirstName} {delegateUser.LastName}" : delegateUser.LastName; BodyBuilder body = new BodyBuilder { TextBody = $@"Dear {nameToUse}, - An administrator has registered your details to give you access to the Digital Learning Solutions (DLS) platform under the centre {centreName}. - You have been assigned the unique DLS delegate number {candidateNumber}. + An administrator has registered your details to give you access to the Digital Learning Solutions (DLS) platform under the centre {delegateUser.CentreName}. + You have been assigned the unique DLS delegate number {delegateUser.CandidateNumber}. To complete your registration and access your Digital Learning Solutions content, please click: {setPasswordUrl.Uri} Note that this link can only be used once and it will expire in three days. Please don't reply to this email as it has been automatically generated.", HtmlBody = $@"

Dear {nameToUse},

-

An administrator has registered your details to give you access to the Digital Learning Solutions (DLS) platform under the centre {centreName}.

-

You have been assigned the unique DLS delegate number {candidateNumber}.

+

An administrator has registered your details to give you access to the Digital Learning Solutions (DLS) platform under the centre {delegateUser.CentreName}.

+

You have been assigned the unique DLS delegate number {delegateUser.CandidateNumber}.

To complete your registration and access your Digital Learning Solutions content, please click this link.

Note that this link can only be used once and it will expire in three days.

Please don't reply to this email as it has been automatically generated.

diff --git a/DigitalLearningSolutions.Data/Services/RegistrationService.cs b/DigitalLearningSolutions.Data/Services/RegistrationService.cs index c87c86431a..0619e2e63f 100644 --- a/DigitalLearningSolutions.Data/Services/RegistrationService.cs +++ b/DigitalLearningSolutions.Data/Services/RegistrationService.cs @@ -28,13 +28,11 @@ public class RegistrationService : IRegistrationService private readonly IConfiguration config; private readonly IEmailService emailService; private readonly IPasswordDataService passwordDataService; - private readonly IPasswordResetService passwordResetService; private readonly IRegistrationDataService registrationDataService; public RegistrationService( IRegistrationDataService registrationDataService, IPasswordDataService passwordDataService, - IPasswordResetService passwordResetService, IEmailService emailService, ICentresDataService centresDataService, IConfiguration config @@ -42,7 +40,6 @@ IConfiguration config { this.registrationDataService = registrationDataService; this.passwordDataService = passwordDataService; - this.passwordResetService = passwordResetService; this.emailService = emailService; this.centresDataService = centresDataService; this.config = config; diff --git a/DigitalLearningSolutions.Web.Tests/Controllers/ResetPasswordControllerTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/ResetPasswordControllerTests.cs index e7496185b5..a9eb9afdbf 100644 --- a/DigitalLearningSolutions.Web.Tests/Controllers/ResetPasswordControllerTests.cs +++ b/DigitalLearningSolutions.Web.Tests/Controllers/ResetPasswordControllerTests.cs @@ -1,8 +1,9 @@ namespace DigitalLearningSolutions.Web.Tests.Controllers { using System.Threading.Tasks; + using DigitalLearningSolutions.Data.Helpers; using DigitalLearningSolutions.Data.Services; - using DigitalLearningSolutions.Web.Controllers; + using DigitalLearningSolutions.Web.Controllers.SetPassword; using DigitalLearningSolutions.Web.Extensions; using DigitalLearningSolutions.Web.Models; using DigitalLearningSolutions.Web.Tests.ControllerHelpers; @@ -15,9 +16,9 @@ namespace DigitalLearningSolutions.Web.Tests.Controllers public class ResetPasswordControllerTests { private ResetPasswordController authenticatedController = null!; - private ResetPasswordController unauthenticatedController = null!; private IPasswordResetService passwordResetService = null!; private IPasswordService passwordService = null!; + private ResetPasswordController unauthenticatedController = null!; [SetUp] public void SetUp() @@ -49,7 +50,13 @@ public async Task Index_should_redirect_to_homepage_if_user_is_authenticated() public async Task Index_should_render_if_user_is_unauthenticated_and_query_params_are_valid() { // Given - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "code", false)) + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "code", + ResetPasswordHelpers.ResetPasswordHashExpiryTime + ) + ) .Returns(Task.FromResult(true)); // When @@ -63,7 +70,13 @@ public async Task Index_should_render_if_user_is_unauthenticated_and_query_param public async Task Index_should_redirect_to_error_page_if_user_is_unauthenticated_and_query_params_are_invalid() { // Given - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "code", false)) + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "code", + ResetPasswordHelpers.ResetPasswordHashExpiryTime + ) + ) .Returns(Task.FromResult(false)); // When @@ -97,7 +110,13 @@ public async Task Index_should_redirect_to_login_page_if_user_is_unauthenticated public async Task Index_should_set_email_and_hash_in_temp_data_if_valid() { // Given - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", false)) + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "hash", + ResetPasswordHelpers.ResetPasswordHashExpiryTime + ) + ) .Returns(true); // When @@ -105,19 +124,28 @@ public async Task Index_should_set_email_and_hash_in_temp_data_if_valid() // Then unauthenticatedController.TempData.Peek().Should().BeEquivalentTo( - new ResetPasswordData("email", "hash")); + new ResetPasswordData("email", "hash") + ); } [Test] public async Task Post_to_index_should_invalidate_reset_hash_if_model_and_hash_valid() { // Given - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", false)).Returns(true); + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "hash", + ResetPasswordHelpers.ResetPasswordHashExpiryTime + ) + ) + .Returns(true); unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); // When await unauthenticatedController.Index( - new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" } + ); // Then A.CallTo(() => passwordResetService.InvalidateResetPasswordForEmailAsync("email")) @@ -129,11 +157,19 @@ public async Task Post_to_index_should_update_password_if_model_and_hash_valid() { // Given unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", false)).Returns(true); + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "hash", + ResetPasswordHelpers.ResetPasswordHashExpiryTime + ) + ) + .Returns(true); // When await unauthenticatedController.Index( - new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" } + ); // Then A.CallTo(() => passwordService.ChangePasswordAsync("email", "testPass-9")) @@ -145,11 +181,19 @@ public async Task Post_to_index_should_return_success_page_if_model_and_hash_val { // Given unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", false)).Returns(true); + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "hash", + ResetPasswordHelpers.ResetPasswordHashExpiryTime + ) + ) + .Returns(true); // When var result = await unauthenticatedController.Index( - new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" } + ); // Then result.Should().BeViewResult().WithViewName("Success"); @@ -161,11 +205,19 @@ public async Task Post_to_index_should_clear_temp_data_if_model_and_hash_valid() // Given unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); unauthenticatedController.TempData.Set("some string"); - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", false)).Returns(true); + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "hash", + ResetPasswordHelpers.ResetPasswordHashExpiryTime + ) + ) + .Returns(true); // When await unauthenticatedController.Index( - new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" } + ); // Then unauthenticatedController.TempData.Peek().Should().BeNull(); @@ -178,11 +230,19 @@ public async Task Post_to_index_should_clear_temp_data_if_hash_invalid() // Given unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); unauthenticatedController.TempData.Set("some string"); - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", false)).Returns(false); + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "hash", + ResetPasswordHelpers.ResetPasswordHashExpiryTime + ) + ) + .Returns(false); // When await unauthenticatedController.Index( - new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" } + ); // Then unauthenticatedController.TempData.Peek().Should().BeNull(); @@ -194,12 +254,20 @@ public async Task Post_to_index_should_preserve_temp_data_if_model_invalid() { // Given unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", false)).Returns(true); + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "hash", + ResetPasswordHelpers.ResetPasswordHashExpiryTime + ) + ) + .Returns(true); unauthenticatedController.ModelState.AddModelError("model", "Invalid for testing"); // When await unauthenticatedController.Index( - new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" } + ); // Then unauthenticatedController.TempData.Peek().Should() @@ -212,11 +280,19 @@ public async Task Post_to_index_should_return_form_if_model_state_invalid() // Given unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); unauthenticatedController.ModelState.AddModelError("Testings", "errors for testing"); - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", false)).Returns(true); + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "hash", + ResetPasswordHelpers.ResetPasswordHashExpiryTime + ) + ) + .Returns(true); // When var result = await unauthenticatedController.Index( - new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" } + ); // Then result.Should().BeViewResult().WithDefaultViewName(); @@ -226,12 +302,20 @@ public async Task Post_to_index_should_return_form_if_model_state_invalid() public async Task Post_to_index_should_redirect_to_Error_if_reset_password_invalid() { // Given - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", false)).Returns(false); + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "hash", + ResetPasswordHelpers.ResetPasswordHashExpiryTime + ) + ) + .Returns(false); unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); // When var result = await unauthenticatedController.Index( - new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" } + ); // Then result.Should().BeRedirectToActionResult().WithActionName("Error").WithControllerName(null); diff --git a/DigitalLearningSolutions.Web.Tests/Controllers/SetPasswordControllerTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/SetPasswordControllerTests.cs index 25a6579c68..569ae0dcf6 100644 --- a/DigitalLearningSolutions.Web.Tests/Controllers/SetPasswordControllerTests.cs +++ b/DigitalLearningSolutions.Web.Tests/Controllers/SetPasswordControllerTests.cs @@ -1,8 +1,9 @@ namespace DigitalLearningSolutions.Web.Tests.Controllers { using System.Threading.Tasks; + using DigitalLearningSolutions.Data.Helpers; using DigitalLearningSolutions.Data.Services; - using DigitalLearningSolutions.Web.Controllers; + using DigitalLearningSolutions.Web.Controllers.SetPassword; using DigitalLearningSolutions.Web.Extensions; using DigitalLearningSolutions.Web.Models; using DigitalLearningSolutions.Web.Tests.ControllerHelpers; @@ -15,9 +16,9 @@ public class SetPasswordControllerTests { private SetPasswordController authenticatedController = null!; - private SetPasswordController unauthenticatedController = null!; private IPasswordResetService passwordResetService = null!; private IPasswordService passwordService = null!; + private SetPasswordController unauthenticatedController = null!; [SetUp] public void SetUp() @@ -49,7 +50,13 @@ public async Task Index_should_redirect_to_homepage_if_user_is_authenticated() public async Task Index_should_render_if_user_is_unauthenticated_and_query_params_are_valid() { // Given - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "code", true)) + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "code", + ResetPasswordHelpers.SetPasswordHasExpiryTime + ) + ) .Returns(Task.FromResult(true)); // When @@ -63,7 +70,13 @@ public async Task Index_should_render_if_user_is_unauthenticated_and_query_param public async Task Index_should_redirect_to_error_page_if_user_is_unauthenticated_and_query_params_are_invalid() { // Given - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "code", true)) + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "code", + ResetPasswordHelpers.SetPasswordHasExpiryTime + ) + ) .Returns(Task.FromResult(false)); // When @@ -97,7 +110,13 @@ public async Task Index_should_redirect_to_login_page_if_user_is_unauthenticated public async Task Index_should_set_email_and_hash_in_temp_data_if_valid() { // Given - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", true)) + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "hash", + ResetPasswordHelpers.SetPasswordHasExpiryTime + ) + ) .Returns(true); // When @@ -105,19 +124,28 @@ public async Task Index_should_set_email_and_hash_in_temp_data_if_valid() // Then unauthenticatedController.TempData.Peek().Should().BeEquivalentTo( - new ResetPasswordData("email", "hash")); + new ResetPasswordData("email", "hash") + ); } [Test] public async Task Post_to_index_should_invalidate_reset_hash_if_model_and_hash_valid() { // Given - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", true)).Returns(true); + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "hash", + ResetPasswordHelpers.SetPasswordHasExpiryTime + ) + ) + .Returns(true); unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); // When await unauthenticatedController.Index( - new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" } + ); // Then A.CallTo(() => passwordResetService.InvalidateResetPasswordForEmailAsync("email")) @@ -129,11 +157,19 @@ public async Task Post_to_index_should_update_password_if_model_and_hash_valid() { // Given unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", true)).Returns(true); + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "hash", + ResetPasswordHelpers.SetPasswordHasExpiryTime + ) + ) + .Returns(true); // When await unauthenticatedController.Index( - new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" } + ); // Then A.CallTo(() => passwordService.ChangePasswordAsync("email", "testPass-9")) @@ -145,11 +181,19 @@ public async Task Post_to_index_should_return_success_page_if_model_and_hash_val { // Given unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", true)).Returns(true); + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "hash", + ResetPasswordHelpers.SetPasswordHasExpiryTime + ) + ) + .Returns(true); // When var result = await unauthenticatedController.Index( - new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" } + ); // Then result.Should().BeViewResult().WithViewName("Success"); @@ -161,11 +205,19 @@ public async Task Post_to_index_should_clear_temp_data_if_model_and_hash_valid() // Given unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); unauthenticatedController.TempData.Set("some string"); - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", true)).Returns(true); + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "hash", + ResetPasswordHelpers.SetPasswordHasExpiryTime + ) + ) + .Returns(true); // When await unauthenticatedController.Index( - new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" } + ); // Then unauthenticatedController.TempData.Peek().Should().BeNull(); @@ -178,11 +230,19 @@ public async Task Post_to_index_should_clear_temp_data_if_hash_invalid() // Given unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); unauthenticatedController.TempData.Set("some string"); - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", true)).Returns(false); + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "hash", + ResetPasswordHelpers.SetPasswordHasExpiryTime + ) + ) + .Returns(false); // When await unauthenticatedController.Index( - new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" } + ); // Then unauthenticatedController.TempData.Peek().Should().BeNull(); @@ -194,12 +254,20 @@ public async Task Post_to_index_should_preserve_temp_data_if_model_invalid() { // Given unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", true)).Returns(true); + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "hash", + ResetPasswordHelpers.SetPasswordHasExpiryTime + ) + ) + .Returns(true); unauthenticatedController.ModelState.AddModelError("model", "Invalid for testing"); // When await unauthenticatedController.Index( - new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" } + ); // Then unauthenticatedController.TempData.Peek().Should() @@ -212,11 +280,19 @@ public async Task Post_to_index_should_return_form_if_model_state_invalid() // Given unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); unauthenticatedController.ModelState.AddModelError("Testings", "errors for testing"); - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", true)).Returns(true); + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "hash", + ResetPasswordHelpers.SetPasswordHasExpiryTime + ) + ) + .Returns(true); // When var result = await unauthenticatedController.Index( - new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" } + ); // Then result.Should().BeViewResult().WithDefaultViewName(); @@ -226,12 +302,20 @@ public async Task Post_to_index_should_return_form_if_model_state_invalid() public async Task Post_to_index_should_redirect_to_Error_if_reset_password_invalid() { // Given - A.CallTo(() => passwordResetService.EmailAndResetPasswordHashAreValidAsync("email", "hash", true)).Returns(false); + A.CallTo( + () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( + "email", + "hash", + ResetPasswordHelpers.SetPasswordHasExpiryTime + ) + ) + .Returns(false); unauthenticatedController.TempData.Set(new ResetPasswordData("email", "hash")); // When var result = await unauthenticatedController.Index( - new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" }); + new ConfirmPasswordViewModel { Password = "testPass-9", ConfirmPassword = "testPass-9" } + ); // Then result.Should().BeRedirectToActionResult().WithActionName("Error").WithControllerName(null); diff --git a/DigitalLearningSolutions.Web/Controllers/ForgotPasswordController.cs b/DigitalLearningSolutions.Web/Controllers/ForgotPasswordController.cs index dcd83805fa..6b763eb23f 100644 --- a/DigitalLearningSolutions.Web/Controllers/ForgotPasswordController.cs +++ b/DigitalLearningSolutions.Web/Controllers/ForgotPasswordController.cs @@ -34,7 +34,7 @@ public IActionResult Index(ForgotPasswordViewModel model) return View(model); } - string baseUrl = ConfigHelper.GetAppConfig()["AppRootPath"]; + string baseUrl = ConfigHelper.GetAppConfig().GetAppRootPath(); try { diff --git a/DigitalLearningSolutions.Web/Controllers/ResetPasswordController.cs b/DigitalLearningSolutions.Web/Controllers/SetPassword/ResetPasswordController.cs similarity index 54% rename from DigitalLearningSolutions.Web/Controllers/ResetPasswordController.cs rename to DigitalLearningSolutions.Web/Controllers/SetPassword/ResetPasswordController.cs index 66a6b3889d..265320d84e 100644 --- a/DigitalLearningSolutions.Web/Controllers/ResetPasswordController.cs +++ b/DigitalLearningSolutions.Web/Controllers/SetPassword/ResetPasswordController.cs @@ -1,6 +1,7 @@ -namespace DigitalLearningSolutions.Web.Controllers +namespace DigitalLearningSolutions.Web.Controllers.SetPassword { using System.Threading.Tasks; + using DigitalLearningSolutions.Data.Helpers; using DigitalLearningSolutions.Data.Services; using DigitalLearningSolutions.Web.Extensions; using DigitalLearningSolutions.Web.Models; @@ -8,10 +9,19 @@ namespace DigitalLearningSolutions.Web.Controllers using DigitalLearningSolutions.Web.ViewModels.Common; using Microsoft.AspNetCore.Mvc; - public class ResetPasswordController : SetPasswordControllerBase + public class ResetPasswordController : Controller { - public ResetPasswordController(IPasswordResetService passwordResetService, IPasswordService passwordService) : - base(passwordResetService, passwordService) { } + private readonly IPasswordResetService passwordResetService; + private readonly IPasswordService passwordService; + + public ResetPasswordController( + IPasswordResetService passwordResetService, + IPasswordService passwordService + ) + { + this.passwordResetService = passwordResetService; + this.passwordService = passwordService; + } [HttpGet] public async Task Index(string email, string code) @@ -26,7 +36,11 @@ public async Task Index(string email, string code) return RedirectToAction("Index", "Login"); } - var hashIsValid = await IsSetPasswordLinkValid(email, code); + var hashIsValid = await passwordResetService.EmailAndResetPasswordHashAreValidAsync( + email, + code, + ResetPasswordHelpers.ResetPasswordHashExpiryTime + ); TempData.Set(new ResetPasswordData(email, code)); @@ -44,7 +58,11 @@ public async Task Index(ConfirmPasswordViewModel viewModel) { var resetPasswordData = TempData.Peek()!; - var hashIsValid = await IsSetPasswordLinkValid(resetPasswordData); + var hashIsValid = await passwordResetService.EmailAndResetPasswordHashAreValidAsync( + resetPasswordData.Email, + resetPasswordData.ResetPasswordHash, + ResetPasswordHelpers.ResetPasswordHashExpiryTime + ); if (!hashIsValid) { @@ -57,7 +75,10 @@ public async Task Index(ConfirmPasswordViewModel viewModel) return View(viewModel); } - await ChangePassword(viewModel, resetPasswordData); + await passwordResetService.InvalidateResetPasswordForEmailAsync(resetPasswordData.Email); + await passwordService.ChangePasswordAsync(resetPasswordData.Email, viewModel.Password!); + + TempData.Clear(); return View("Success"); } diff --git a/DigitalLearningSolutions.Web/Controllers/SetPasswordController.cs b/DigitalLearningSolutions.Web/Controllers/SetPassword/SetPasswordController.cs similarity index 54% rename from DigitalLearningSolutions.Web/Controllers/SetPasswordController.cs rename to DigitalLearningSolutions.Web/Controllers/SetPassword/SetPasswordController.cs index 0b7f4a89f0..561fa5caac 100644 --- a/DigitalLearningSolutions.Web/Controllers/SetPasswordController.cs +++ b/DigitalLearningSolutions.Web/Controllers/SetPassword/SetPasswordController.cs @@ -1,6 +1,7 @@ -namespace DigitalLearningSolutions.Web.Controllers +namespace DigitalLearningSolutions.Web.Controllers.SetPassword { using System.Threading.Tasks; + using DigitalLearningSolutions.Data.Helpers; using DigitalLearningSolutions.Data.Services; using DigitalLearningSolutions.Web.Extensions; using DigitalLearningSolutions.Web.Models; @@ -8,10 +9,19 @@ namespace DigitalLearningSolutions.Web.Controllers using DigitalLearningSolutions.Web.ViewModels.Common; using Microsoft.AspNetCore.Mvc; - public class SetPasswordController : SetPasswordControllerBase + public class SetPasswordController : Controller { - public SetPasswordController(IPasswordResetService passwordResetService, IPasswordService passwordService) : - base(passwordResetService, passwordService) { } + private readonly IPasswordResetService passwordResetService; + private readonly IPasswordService passwordService; + + public SetPasswordController( + IPasswordResetService passwordResetService, + IPasswordService passwordService + ) + { + this.passwordResetService = passwordResetService; + this.passwordService = passwordService; + } [HttpGet] public async Task Index(string email, string code) @@ -26,7 +36,11 @@ public async Task Index(string email, string code) return RedirectToAction("Index", "Login"); } - var hashIsValid = await IsSetPasswordLinkValid(email, code, true); + var hashIsValid = await passwordResetService.EmailAndResetPasswordHashAreValidAsync( + email, + code, + ResetPasswordHelpers.SetPasswordHasExpiryTime + ); TempData.Set(new ResetPasswordData(email, code)); @@ -44,7 +58,11 @@ public async Task Index(ConfirmPasswordViewModel viewModel) { var resetPasswordData = TempData.Peek()!; - var hashIsValid = await IsSetPasswordLinkValid(resetPasswordData, true); + var hashIsValid = await passwordResetService.EmailAndResetPasswordHashAreValidAsync( + resetPasswordData.Email, + resetPasswordData.ResetPasswordHash, + ResetPasswordHelpers.SetPasswordHasExpiryTime + ); if (!hashIsValid) { @@ -57,13 +75,14 @@ public async Task Index(ConfirmPasswordViewModel viewModel) return View(viewModel); } - await ChangePassword(viewModel, resetPasswordData); + await passwordResetService.InvalidateResetPasswordForEmailAsync(resetPasswordData.Email); + await passwordService.ChangePasswordAsync(resetPasswordData.Email, viewModel.Password!); + + TempData.Clear(); return View("Success"); } - - [HttpGet] public IActionResult Error() { diff --git a/DigitalLearningSolutions.Web/Controllers/SetPasswordControllerBase.cs b/DigitalLearningSolutions.Web/Controllers/SetPasswordControllerBase.cs deleted file mode 100644 index a851f9ab91..0000000000 --- a/DigitalLearningSolutions.Web/Controllers/SetPasswordControllerBase.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace DigitalLearningSolutions.Web.Controllers -{ - using System.Threading.Tasks; - using DigitalLearningSolutions.Data.Services; - using DigitalLearningSolutions.Web.Models; - using DigitalLearningSolutions.Web.ViewModels.Common; - using Microsoft.AspNetCore.Mvc; - - public abstract class SetPasswordControllerBase : Controller - { - protected readonly IPasswordResetService PasswordResetService; - protected readonly IPasswordService PasswordService; - - protected SetPasswordControllerBase( - IPasswordResetService passwordResetService, - IPasswordService passwordService - ) - { - PasswordResetService = passwordResetService; - PasswordService = passwordService; - } - - protected async Task IsSetPasswordLinkValid( - string emailAddress, - string resetHash, - bool isSetPassword = false - ) - { - return await PasswordResetService.EmailAndResetPasswordHashAreValidAsync( - emailAddress, - resetHash, - isSetPassword - ); - } - - protected async Task IsSetPasswordLinkValid( - ResetPasswordData resetPasswordData, - bool isSetPassword = false - ) - { - return await IsSetPasswordLinkValid( - resetPasswordData.Email, - resetPasswordData.ResetPasswordHash, - isSetPassword - ); - } - - protected async Task ChangePassword(ConfirmPasswordViewModel viewModel, ResetPasswordData resetPasswordData) - { - await PasswordResetService.InvalidateResetPasswordForEmailAsync(resetPasswordData.Email); - await PasswordService.ChangePasswordAsync(resetPasswordData.Email, viewModel.Password!); - - TempData.Clear(); - } - } -} diff --git a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/ViewDelegateController.cs b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/ViewDelegateController.cs index f04be6d4c0..3bf46c4a9d 100644 --- a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/ViewDelegateController.cs +++ b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/ViewDelegateController.cs @@ -3,9 +3,7 @@ using System.Linq; using DigitalLearningSolutions.Data.DataServices.UserDataService; using DigitalLearningSolutions.Data.Services; - using DigitalLearningSolutions.Web.Extensions; using DigitalLearningSolutions.Web.Helpers; - using DigitalLearningSolutions.Web.ServiceFilter; using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -65,7 +63,7 @@ public IActionResult SendWelcomeEmail(int delegateId) return new NotFoundResult(); } - string baseUrl = ConfigHelper.GetAppConfig()["AppRootPath"]; + string baseUrl = ConfigHelper.GetAppConfig().GetAppRootPath(); passwordResetService.GenerateAndSendDelegateWelcomeEmail(delegateUser.EmailAddress!, baseUrl); diff --git a/DigitalLearningSolutions.Web/Helpers/ConfigHelper.cs b/DigitalLearningSolutions.Web/Helpers/ConfigHelper.cs index 63e0c94c94..d6973f4fcd 100644 --- a/DigitalLearningSolutions.Web/Helpers/ConfigHelper.cs +++ b/DigitalLearningSolutions.Web/Helpers/ConfigHelper.cs @@ -1,6 +1,7 @@ namespace DigitalLearningSolutions.Web.Helpers { using System; + using System.Drawing; using System.IO; using Microsoft.Extensions.Configuration; using static System.String; @@ -63,5 +64,10 @@ public static string GetMapsApiKey(this IConfiguration config) { return config[MapsApiKey]; } + + public static string GetAppRootPath(this IConfiguration config) + { + return config[AppRootPathName]; + } } } From 64e5501363a7c222ff94cfdc257537895cb6da46 Mon Sep 17 00:00:00 2001 From: Alex Jackson Date: Wed, 18 Aug 2021 09:02:35 +0100 Subject: [PATCH 9/9] HEEDLS-495 Fis naming of expiry value --- .../Helpers/ResetPasswordHelperTests.cs | 4 ++-- .../Helpers/ResetPasswordHelpers.cs | 2 +- .../Controllers/SetPasswordControllerTests.cs | 22 +++++++++---------- .../SetPassword/SetPasswordController.cs | 4 ++-- .../Helpers/ConfigHelper.cs | 1 - 5 files changed, 16 insertions(+), 17 deletions(-) diff --git a/DigitalLearningSolutions.Data.Tests/Helpers/ResetPasswordHelperTests.cs b/DigitalLearningSolutions.Data.Tests/Helpers/ResetPasswordHelperTests.cs index 6db5f5b56d..3dbed4e2bf 100644 --- a/DigitalLearningSolutions.Data.Tests/Helpers/ResetPasswordHelperTests.cs +++ b/DigitalLearningSolutions.Data.Tests/Helpers/ResetPasswordHelperTests.cs @@ -51,7 +51,7 @@ public void Set_Password_Is_Valid_4319_Minutes_After_Creation() .With(rp => rp.PasswordResetDateTime = createTime).Build(); // When - var resetIsValid = resetPassword.IsStillValidAt(createTime + TimeSpan.FromMinutes(4319), ResetPasswordHelpers.SetPasswordHasExpiryTime); + var resetIsValid = resetPassword.IsStillValidAt(createTime + TimeSpan.FromMinutes(4319), ResetPasswordHelpers.SetPasswordHashExpiryTime); // Then resetIsValid.Should().BeTrue(); @@ -66,7 +66,7 @@ public void Set_Password_Is_Invalid_121_Minutes_After_Creation() .With(rp => rp.PasswordResetDateTime = createTime).Build(); // When - var resetIsValid = resetPassword.IsStillValidAt(createTime + TimeSpan.FromMinutes(4321), ResetPasswordHelpers.SetPasswordHasExpiryTime); + var resetIsValid = resetPassword.IsStillValidAt(createTime + TimeSpan.FromMinutes(4321), ResetPasswordHelpers.SetPasswordHashExpiryTime); // Then resetIsValid.Should().BeFalse(); diff --git a/DigitalLearningSolutions.Data/Helpers/ResetPasswordHelpers.cs b/DigitalLearningSolutions.Data/Helpers/ResetPasswordHelpers.cs index b009e49c55..52d0274ca0 100644 --- a/DigitalLearningSolutions.Data/Helpers/ResetPasswordHelpers.cs +++ b/DigitalLearningSolutions.Data/Helpers/ResetPasswordHelpers.cs @@ -9,7 +9,7 @@ namespace DigitalLearningSolutions.Data.Helpers public static class ResetPasswordHelpers { public static readonly TimeSpan ResetPasswordHashExpiryTime = TimeSpan.FromHours(2); - public static readonly TimeSpan SetPasswordHasExpiryTime = TimeSpan.FromDays(3); + public static readonly TimeSpan SetPasswordHashExpiryTime = TimeSpan.FromDays(3); public static bool IsStillValidAt(this ResetPassword passwordReset, DateTime dateTime, TimeSpan expiryTime) { diff --git a/DigitalLearningSolutions.Web.Tests/Controllers/SetPasswordControllerTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/SetPasswordControllerTests.cs index 569ae0dcf6..be55c0d8e8 100644 --- a/DigitalLearningSolutions.Web.Tests/Controllers/SetPasswordControllerTests.cs +++ b/DigitalLearningSolutions.Web.Tests/Controllers/SetPasswordControllerTests.cs @@ -54,7 +54,7 @@ public async Task Index_should_render_if_user_is_unauthenticated_and_query_param () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( "email", "code", - ResetPasswordHelpers.SetPasswordHasExpiryTime + ResetPasswordHelpers.SetPasswordHashExpiryTime ) ) .Returns(Task.FromResult(true)); @@ -74,7 +74,7 @@ public async Task Index_should_redirect_to_error_page_if_user_is_unauthenticated () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( "email", "code", - ResetPasswordHelpers.SetPasswordHasExpiryTime + ResetPasswordHelpers.SetPasswordHashExpiryTime ) ) .Returns(Task.FromResult(false)); @@ -114,7 +114,7 @@ public async Task Index_should_set_email_and_hash_in_temp_data_if_valid() () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( "email", "hash", - ResetPasswordHelpers.SetPasswordHasExpiryTime + ResetPasswordHelpers.SetPasswordHashExpiryTime ) ) .Returns(true); @@ -136,7 +136,7 @@ public async Task Post_to_index_should_invalidate_reset_hash_if_model_and_hash_v () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( "email", "hash", - ResetPasswordHelpers.SetPasswordHasExpiryTime + ResetPasswordHelpers.SetPasswordHashExpiryTime ) ) .Returns(true); @@ -161,7 +161,7 @@ public async Task Post_to_index_should_update_password_if_model_and_hash_valid() () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( "email", "hash", - ResetPasswordHelpers.SetPasswordHasExpiryTime + ResetPasswordHelpers.SetPasswordHashExpiryTime ) ) .Returns(true); @@ -185,7 +185,7 @@ public async Task Post_to_index_should_return_success_page_if_model_and_hash_val () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( "email", "hash", - ResetPasswordHelpers.SetPasswordHasExpiryTime + ResetPasswordHelpers.SetPasswordHashExpiryTime ) ) .Returns(true); @@ -209,7 +209,7 @@ public async Task Post_to_index_should_clear_temp_data_if_model_and_hash_valid() () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( "email", "hash", - ResetPasswordHelpers.SetPasswordHasExpiryTime + ResetPasswordHelpers.SetPasswordHashExpiryTime ) ) .Returns(true); @@ -234,7 +234,7 @@ public async Task Post_to_index_should_clear_temp_data_if_hash_invalid() () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( "email", "hash", - ResetPasswordHelpers.SetPasswordHasExpiryTime + ResetPasswordHelpers.SetPasswordHashExpiryTime ) ) .Returns(false); @@ -258,7 +258,7 @@ public async Task Post_to_index_should_preserve_temp_data_if_model_invalid() () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( "email", "hash", - ResetPasswordHelpers.SetPasswordHasExpiryTime + ResetPasswordHelpers.SetPasswordHashExpiryTime ) ) .Returns(true); @@ -284,7 +284,7 @@ public async Task Post_to_index_should_return_form_if_model_state_invalid() () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( "email", "hash", - ResetPasswordHelpers.SetPasswordHasExpiryTime + ResetPasswordHelpers.SetPasswordHashExpiryTime ) ) .Returns(true); @@ -306,7 +306,7 @@ public async Task Post_to_index_should_redirect_to_Error_if_reset_password_inval () => passwordResetService.EmailAndResetPasswordHashAreValidAsync( "email", "hash", - ResetPasswordHelpers.SetPasswordHasExpiryTime + ResetPasswordHelpers.SetPasswordHashExpiryTime ) ) .Returns(false); diff --git a/DigitalLearningSolutions.Web/Controllers/SetPassword/SetPasswordController.cs b/DigitalLearningSolutions.Web/Controllers/SetPassword/SetPasswordController.cs index 561fa5caac..aa701c3d75 100644 --- a/DigitalLearningSolutions.Web/Controllers/SetPassword/SetPasswordController.cs +++ b/DigitalLearningSolutions.Web/Controllers/SetPassword/SetPasswordController.cs @@ -39,7 +39,7 @@ public async Task Index(string email, string code) var hashIsValid = await passwordResetService.EmailAndResetPasswordHashAreValidAsync( email, code, - ResetPasswordHelpers.SetPasswordHasExpiryTime + ResetPasswordHelpers.SetPasswordHashExpiryTime ); TempData.Set(new ResetPasswordData(email, code)); @@ -61,7 +61,7 @@ public async Task Index(ConfirmPasswordViewModel viewModel) var hashIsValid = await passwordResetService.EmailAndResetPasswordHashAreValidAsync( resetPasswordData.Email, resetPasswordData.ResetPasswordHash, - ResetPasswordHelpers.SetPasswordHasExpiryTime + ResetPasswordHelpers.SetPasswordHashExpiryTime ); if (!hashIsValid) diff --git a/DigitalLearningSolutions.Web/Helpers/ConfigHelper.cs b/DigitalLearningSolutions.Web/Helpers/ConfigHelper.cs index d6973f4fcd..53a2fe9d6b 100644 --- a/DigitalLearningSolutions.Web/Helpers/ConfigHelper.cs +++ b/DigitalLearningSolutions.Web/Helpers/ConfigHelper.cs @@ -1,7 +1,6 @@ namespace DigitalLearningSolutions.Web.Helpers { using System; - using System.Drawing; using System.IO; using Microsoft.Extensions.Configuration; using static System.String;