diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 6295133d2..aeb87418d 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -403,6 +403,84 @@ namespace BTCPayServer.Tests Assert.Contains("/login", s.Driver.Url); } + [Fact(Timeout = TestTimeout)] + public async Task CanManageUsers() + { + using var s = CreateSeleniumTester(); + await s.StartAsync(); + //Create Users + s.RegisterNewUser(); + var user = s.AsTestAccount(); + s.Logout(); + s.GoToRegister(); + s.RegisterNewUser(true); + var admin = s.AsTestAccount(); + s.GoToHome(); + s.GoToServer(ServerNavPages.Users); + + // Manage user password reset + var rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row")); + s.Driver.FindElement(By.Id("SearchTerm")).Clear(); + s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(user.RegisterDetails.Email); + s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter); + rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row")); + Assert.Single(rows); + Assert.Contains(user.RegisterDetails.Email, rows.First().Text); + s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .reset-password")).Click(); + s.Driver.WaitForElement(By.Id("Password")).SendKeys("Password@1!"); + s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("Password@1!"); + s.ClickPagePrimary(); + Assert.Contains("Password successfully set", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success).Text); + + // Manage user status (disable and enable) + // Disable user + s.Driver.FindElement(By.Id("SearchTerm")).Clear(); + s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(user.RegisterDetails.Email); + s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter); + rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row")); + Assert.Single(rows); + Assert.Contains(user.RegisterDetails.Email, rows.First().Text); + s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .disable-user")).Click(); + s.Driver.WaitForElement(By.Id("ConfirmContinue")).Click(); + Assert.Contains("User disabled", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success).Text); + //Enable user + s.Driver.FindElement(By.Id("SearchTerm")).Clear(); + s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(user.RegisterDetails.Email); + s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter); + rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row")); + Assert.Single(rows); + Assert.Contains(user.RegisterDetails.Email, rows.First().Text); + s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .enable-user")).Click(); + s.Driver.WaitForElement(By.Id("ConfirmContinue")).Click(); + Assert.Contains("User enabled", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success).Text); + + // Manage user details (edit) + s.Driver.FindElement(By.Id("SearchTerm")).Clear(); + s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(user.RegisterDetails.Email); + s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter); + rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row")); + Assert.Single(rows); + Assert.Contains(user.RegisterDetails.Email, rows.First().Text); + s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .user-edit")).Click(); + s.Driver.WaitForElement(By.Id("Name")).SendKeys("Test User"); + s.ClickPagePrimary(); + Assert.Contains("User successfully updated", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success).Text); + + // Manage user deletion + s.GoToServer(ServerNavPages.Users); + s.Driver.FindElement(By.Id("SearchTerm")).Clear(); + s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(user.RegisterDetails.Email); + s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter); + rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row")); + Assert.Single(rows); + Assert.Contains(user.RegisterDetails.Email, rows.First().Text); + s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .delete-user")).Click(); + s.Driver.WaitForElement(By.Id("ConfirmContinue")).Click(); + Assert.Contains("User deleted", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success).Text); + + s.Driver.AssertNoError(); + } + [Fact(Timeout = TestTimeout)] public async Task CanRequireApprovalForNewAccounts() { diff --git a/BTCPayServer/Controllers/UIAccountController.cs b/BTCPayServer/Controllers/UIAccountController.cs index 662dfc184..7971ded66 100644 --- a/BTCPayServer/Controllers/UIAccountController.cs +++ b/BTCPayServer/Controllers/UIAccountController.cs @@ -16,6 +16,7 @@ using BTCPayServer.Filters; using BTCPayServer.Logging; using BTCPayServer.Models.AccountViewModels; using BTCPayServer.Services; +using BTCPayServer.Services.Mails; using Fido2NetLib; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; @@ -706,7 +707,7 @@ namespace BTCPayServer.Controllers [HttpGet("/login/forgot-password")] [AllowAnonymous] - public IActionResult ForgotPassword() + public ActionResult ForgotPassword() { return View(); } @@ -717,7 +718,8 @@ namespace BTCPayServer.Controllers [RateLimitsFilter(ZoneLimits.ForgotPassword, Scope = RateLimitsScope.RemoteAddress)] public async Task ForgotPassword(ForgotPasswordViewModel model) { - if (ModelState.IsValid) + var settings = await _SettingsRepository.GetSettingAsync(); + if (ModelState.IsValid && settings?.IsComplete() is true) { var user = await _userManager.FindByEmailAsync(model.Email); if (!UserService.TryCanLogin(user, out _)) @@ -739,7 +741,7 @@ namespace BTCPayServer.Controllers [HttpGet("/login/forgot-password/confirm")] [AllowAnonymous] - public IActionResult ForgotPasswordConfirmation() + public ActionResult ForgotPasswordConfirmation() { return View(); } diff --git a/BTCPayServer/Controllers/UIServerController.Users.cs b/BTCPayServer/Controllers/UIServerController.Users.cs index f91e48b19..d8bf5876b 100644 --- a/BTCPayServer/Controllers/UIServerController.Users.cs +++ b/BTCPayServer/Controllers/UIServerController.Users.cs @@ -210,6 +210,32 @@ namespace BTCPayServer.Controllers return RedirectToAction(nameof(User), new { userId }); } + [HttpGet("server/users/{userId}/reset-password")] + public async Task ResetUserPassword(string userId) + { + var user = await _UserManager.FindByIdAsync(userId); + if (user == null) + return NotFound(); + return View(new ResetUserPasswordFromAdmin { Email = user.Email }); + } + + [HttpPost("server/users/{userId}/reset-password")] + public async Task ResetUserPassword(string userId, ResetUserPasswordFromAdmin model) + { + + var user = await _UserManager.FindByEmailAsync(model.Email); + if (user == null || user.Id != userId) + return NotFound(); + + var result = await _UserManager.ResetPasswordAsync(user, await _UserManager.GeneratePasswordResetTokenAsync(user), model.Password); + TempData.SetStatusMessageModel(new StatusMessageModel + { + Severity = result.Succeeded ? StatusMessageModel.StatusSeverity.Success : StatusMessageModel.StatusSeverity.Error, + Message = result.Succeeded ? "Password successfully set" : "An error occurred while resetting user password" + }); + return RedirectToAction(nameof(ListUsers)); + } + [HttpGet("server/users/new")] public async Task CreateUser() { @@ -416,6 +442,24 @@ namespace BTCPayServer.Controllers } } + public class ResetUserPasswordFromAdmin + { + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } + + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } + public class RegisterFromAdminViewModel { [Required] diff --git a/BTCPayServer/Views/UIAccount/ForgotPassword.cshtml b/BTCPayServer/Views/UIAccount/ForgotPassword.cshtml index d01e2a070..98942c834 100644 --- a/BTCPayServer/Views/UIAccount/ForgotPassword.cshtml +++ b/BTCPayServer/Views/UIAccount/ForgotPassword.cshtml @@ -1,28 +1,46 @@ -@model ForgotPasswordViewModel +@using BTCPayServer.Services +@using BTCPayServer.Services.Mails +@using Microsoft.AspNetCore.Mvc.TagHelpers +@model ForgotPasswordViewModel +@inject SettingsRepository SettingsRepository @{ - ViewData["Title"] = "Forgot your password?"; + var isEmailConfigured = (await SettingsRepository.GetSettingAsync())?.IsComplete() is true; + ViewData["Title"] = isEmailConfigured ? "Forgot your password?" : "Email Server Configuration Required"; Layout = "_LayoutSignedOut"; } -

- We all forget passwords every now and then. Just provide email address tied to - your account and we'll start the process of helping you recover your account. -

+@if (isEmailConfigured) +{ +

+ We all forget passwords every now and then. Just provide email address tied to + your account and we'll start the process of helping you recover your account. +

-
- @if (!ViewContext.ModelState.IsValid) - { -
- } -
- - - -
-
- -
-
+
+ @if (!ViewContext.ModelState.IsValid) + { +
+ } +
+ + + +
+
+ +
+
+} +else +{ +

Email password reset functionality is not configured for this server. Please contact the server administrator to assist with account recovery.

+

+ If you are the administrator, please follow these steps to + configure email password resets + or reset your admin password through + command line. +

+}

Log in diff --git a/BTCPayServer/Views/UIServer/ListUsers.cshtml b/BTCPayServer/Views/UIServer/ListUsers.cshtml index 0b7060bca..7d9c7b897 100644 --- a/BTCPayServer/Views/UIServer/ListUsers.cshtml +++ b/BTCPayServer/Views/UIServer/ListUsers.cshtml @@ -86,9 +86,11 @@ Edit @if (status.Item2 != "warning") { - @(user.Disabled ? "Enable" : "Disable") + @(user.Disabled ? "Enable" : "Disable") } - Remove + Password Reset + Remove diff --git a/BTCPayServer/Views/UIServer/ResetUserPassword.cshtml b/BTCPayServer/Views/UIServer/ResetUserPassword.cshtml new file mode 100644 index 000000000..5e38fcf72 --- /dev/null +++ b/BTCPayServer/Views/UIServer/ResetUserPassword.cshtml @@ -0,0 +1,48 @@ +@model BTCPayServer.Controllers.ResetUserPasswordFromAdmin +@{ + ViewData.SetActivePage(ServerNavPages.Users, "Reset Password"); +} + +

+ + + +
+
+ @if (!ViewContext.ModelState.IsValid) + { +
+ } +
+ + + +
+
+ + + +
+
+ + + +
+
+
+ + +@section PageFootContent { + +}