mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-13 11:35:51 +01:00
Handle password reset when SMTP isn't configured or validated (#6150)
* Handle password reset when SMTP isn't configured or the configuration cannot be validated * include rel in external a tag * Simplify it * Test fix * Simplify a bit * selenium test to manage users --------- Co-authored-by: Dennis Reimann <mail@dennisreimann.de> Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
This commit is contained in:
parent
7348a6a62f
commit
f07ed53f7e
6 changed files with 217 additions and 25 deletions
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
var settings = await _SettingsRepository.GetSettingAsync<EmailSettings>();
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -210,6 +210,32 @@ namespace BTCPayServer.Controllers
|
|||
return RedirectToAction(nameof(User), new { userId });
|
||||
}
|
||||
|
||||
[HttpGet("server/users/{userId}/reset-password")]
|
||||
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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]
|
||||
|
|
|
@ -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<EmailSettings>())?.IsComplete() is true;
|
||||
ViewData["Title"] = isEmailConfigured ? "Forgot your password?" : "Email Server Configuration Required";
|
||||
Layout = "_LayoutSignedOut";
|
||||
}
|
||||
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
@if (isEmailConfigured)
|
||||
{
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<form asp-action="ForgotPassword" method="post">
|
||||
@if (!ViewContext.ModelState.IsValid)
|
||||
{
|
||||
<div asp-validation-summary="All" class="@(ViewContext.ModelState.ErrorCount.Equals(1) ? "no-marker" : "")"></div>
|
||||
}
|
||||
<div class="form-group">
|
||||
<label asp-for="Email" class="form-label"></label>
|
||||
<input asp-for="Email" class="form-control" />
|
||||
<span asp-validation-for="Email" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group mt-4">
|
||||
<button type="submit" class="btn btn-primary btn-lg w-100">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
<form asp-action="ForgotPassword" method="post">
|
||||
@if (!ViewContext.ModelState.IsValid)
|
||||
{
|
||||
<div asp-validation-summary="All" class="@(ViewContext.ModelState.ErrorCount.Equals(1) ? "no-marker" : "")"></div>
|
||||
}
|
||||
<div class="form-group">
|
||||
<label asp-for="Email" class="form-label"></label>
|
||||
<input asp-for="Email" class="form-control" />
|
||||
<span asp-validation-for="Email" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group mt-4">
|
||||
<button type="submit" class="btn btn-primary btn-lg w-100">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>Email password reset functionality is not configured for this server. Please contact the server administrator to assist with account recovery.</p>
|
||||
<p>
|
||||
If you are the administrator, please follow these steps to
|
||||
<a href="https://docs.btcpayserver.org/Notifications/#smtp-email-setup" target="_blank" rel="noreferrer noopener">configure email password resets</a>
|
||||
or reset your admin password through
|
||||
<a href="https://docs.btcpayserver.org/FAQ/ServerSettings/#forgot-btcpay-admin-password" target="_blank" rel="noreferrer noopener">command line</a>.
|
||||
</p>
|
||||
}
|
||||
|
||||
<p class="text-center mt-2 mb-0">
|
||||
<a id="Login" style="font-size:1.15rem" asp-action="Login" asp-route-returnurl="@ViewData["ReturnUrl"]">Log in</a>
|
||||
|
|
|
@ -86,9 +86,11 @@
|
|||
<a asp-action="User" asp-route-userId="@user.Id" class="user-edit">Edit</a>
|
||||
@if (status.Item2 != "warning")
|
||||
{
|
||||
<a asp-action="ToggleUser" asp-route-userId="@user.Id" asp-route-enable="@user.Disabled">@(user.Disabled ? "Enable" : "Disable")</a>
|
||||
<a asp-action="ToggleUser" asp-route-userId="@user.Id" asp-route-enable="@user.Disabled"
|
||||
class="@(user.Disabled ? "enable-user" : "disable-user")">@(user.Disabled ? "Enable" : "Disable")</a>
|
||||
}
|
||||
<a asp-action="DeleteUser" asp-route-userId="@user.Id">Remove</a>
|
||||
<a asp-action="ResetUserPassword" asp-route-userId="@user.Id" class="reset-password">Password Reset</a>
|
||||
<a asp-action="DeleteUser" asp-route-userId="@user.Id" class="delete-user">Remove</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
|
|
48
BTCPayServer/Views/UIServer/ResetUserPassword.cshtml
Normal file
48
BTCPayServer/Views/UIServer/ResetUserPassword.cshtml
Normal file
|
@ -0,0 +1,48 @@
|
|||
@model BTCPayServer.Controllers.ResetUserPasswordFromAdmin
|
||||
@{
|
||||
ViewData.SetActivePage(ServerNavPages.Users, "Reset Password");
|
||||
}
|
||||
|
||||
<form method="post" asp-action="ResetUserPassword">
|
||||
<div class="sticky-header">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a asp-action="ListUsers">Users</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">@ViewData["Title"]</li>
|
||||
</ol>
|
||||
<h2 text-translate="true">@ViewData["Title"]</h2>
|
||||
</nav>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary" name="command" value="Save">Reset Password</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-6 col-xxl-constrain">
|
||||
@if (!ViewContext.ModelState.IsValid)
|
||||
{
|
||||
<div asp-validation-summary="ModelOnly"></div>
|
||||
}
|
||||
<div class="form-group">
|
||||
<label asp-for="Email" class="form-label"></label>
|
||||
<input asp-for="Email" required="required" class="form-control" readonly />
|
||||
<span asp-validation-for="Email" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Password" class="form-label"></label>
|
||||
<input asp-for="Password" required class="form-control" />
|
||||
<span asp-validation-for="Password" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ConfirmPassword" class="form-label"></label>
|
||||
<input asp-for="ConfirmPassword" required class="form-control" />
|
||||
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@section PageFootContent {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
Loading…
Add table
Reference in a new issue