mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-22 06:21:44 +01:00
Merge pull request #1886 from Kukks/invite-link
Allow admins to invite new users
This commit is contained in:
commit
62f00fa970
20 changed files with 531 additions and 300 deletions
|
@ -12,6 +12,7 @@ using BTCPayServer.Models;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Tests.Logging;
|
using BTCPayServer.Tests.Logging;
|
||||||
using BTCPayServer.Views.Manage;
|
using BTCPayServer.Views.Manage;
|
||||||
|
using BTCPayServer.Views.Server;
|
||||||
using BTCPayServer.Views.Stores;
|
using BTCPayServer.Views.Stores;
|
||||||
using BTCPayServer.Views.Wallets;
|
using BTCPayServer.Views.Wallets;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
|
@ -383,6 +384,16 @@ namespace BTCPayServer.Tests
|
||||||
Driver.FindElement(By.Id($"Wallet{navPages}")).Click();
|
Driver.FindElement(By.Id($"Wallet{navPages}")).Click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void GoToServer(ServerNavPages navPages = ServerNavPages.Index)
|
||||||
|
{
|
||||||
|
Driver.FindElement(By.Id("ServerSettings")).Click();
|
||||||
|
if (navPages != ServerNavPages.Index)
|
||||||
|
{
|
||||||
|
Driver.FindElement(By.Id($"Server-{navPages}")).Click();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public void GoToInvoice(string id)
|
public void GoToInvoice(string id)
|
||||||
{
|
{
|
||||||
|
|
|
@ -8,6 +8,7 @@ using BTCPayServer.Data;
|
||||||
using BTCPayServer.Models;
|
using BTCPayServer.Models;
|
||||||
using BTCPayServer.Services.Wallets;
|
using BTCPayServer.Services.Wallets;
|
||||||
using BTCPayServer.Tests.Logging;
|
using BTCPayServer.Tests.Logging;
|
||||||
|
using BTCPayServer.Views.Server;
|
||||||
using BTCPayServer.Views.Wallets;
|
using BTCPayServer.Views.Wallets;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
|
@ -88,7 +89,7 @@ namespace BTCPayServer.Tests
|
||||||
await s.StartAsync();
|
await s.StartAsync();
|
||||||
//Register & Log Out
|
//Register & Log Out
|
||||||
var email = s.RegisterNewUser();
|
var email = s.RegisterNewUser();
|
||||||
s.Driver.FindElement(By.Id("Logout")).Click();
|
s.Logout();
|
||||||
s.Driver.AssertNoError();
|
s.Driver.AssertNoError();
|
||||||
Assert.Contains("Account/Login", s.Driver.Url);
|
Assert.Contains("Account/Login", s.Driver.Url);
|
||||||
// Should show the Tor address
|
// Should show the Tor address
|
||||||
|
@ -129,7 +130,32 @@ namespace BTCPayServer.Tests
|
||||||
s.Driver.FindElement(By.Id("MySettings")).Click();
|
s.Driver.FindElement(By.Id("MySettings")).Click();
|
||||||
s.ClickOnAllSideMenus();
|
s.ClickOnAllSideMenus();
|
||||||
|
|
||||||
s.Driver.Quit();
|
//let's test invite link
|
||||||
|
s.Logout();
|
||||||
|
s.GoToRegister();
|
||||||
|
var newAdminUser = s.RegisterNewUser(true);
|
||||||
|
s.GoToServer(ServerNavPages.Users);
|
||||||
|
s.Driver.FindElement(By.Id("CreateUser")).Click();
|
||||||
|
|
||||||
|
var usr = RandomUtils.GetUInt256().ToString().Substring(64 - 20) + "@a.com";
|
||||||
|
s.Driver.FindElement(By.Id("Email")).SendKeys(usr);
|
||||||
|
s.Driver.FindElement(By.Id("Save")).Click();
|
||||||
|
var url = s.AssertHappyMessage().FindElement(By.TagName("a")).Text;;
|
||||||
|
s.Logout();
|
||||||
|
s.Driver.Navigate().GoToUrl(url);
|
||||||
|
Assert.Equal("hidden",s.Driver.FindElement(By.Id("Email")).GetAttribute("type"));
|
||||||
|
Assert.Equal(usr,s.Driver.FindElement(By.Id("Email")).GetAttribute("value"));
|
||||||
|
|
||||||
|
s.Driver.FindElement(By.Id("Password")).SendKeys("123456");
|
||||||
|
s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("123456");
|
||||||
|
s.Driver.FindElement(By.Id("SetPassword")).Click();
|
||||||
|
s.AssertHappyMessage();
|
||||||
|
s.Driver.FindElement(By.Id("Email")).SendKeys(usr);
|
||||||
|
s.Driver.FindElement(By.Id("Password")).SendKeys("123456");
|
||||||
|
s.Driver.FindElement(By.Id("LoginButton")).Click();
|
||||||
|
|
||||||
|
// We should be logged in now
|
||||||
|
s.Driver.FindElement(By.Id("mainNav"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,6 @@ using BTCPayServer.Models;
|
||||||
using BTCPayServer.Models.AccountViewModels;
|
using BTCPayServer.Models.AccountViewModels;
|
||||||
using BTCPayServer.Security;
|
using BTCPayServer.Security;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Mails;
|
|
||||||
using BTCPayServer.Services.Stores;
|
|
||||||
using BTCPayServer.U2F;
|
using BTCPayServer.U2F;
|
||||||
using BTCPayServer.U2F.Models;
|
using BTCPayServer.U2F.Models;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
@ -28,8 +26,6 @@ namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
private readonly UserManager<ApplicationUser> _userManager;
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||||
private readonly EmailSenderFactory _EmailSenderFactory;
|
|
||||||
readonly StoreRepository storeRepository;
|
|
||||||
readonly RoleManager<IdentityRole> _RoleManager;
|
readonly RoleManager<IdentityRole> _RoleManager;
|
||||||
readonly SettingsRepository _SettingsRepository;
|
readonly SettingsRepository _SettingsRepository;
|
||||||
readonly Configuration.BTCPayServerOptions _Options;
|
readonly Configuration.BTCPayServerOptions _Options;
|
||||||
|
@ -41,19 +37,15 @@ namespace BTCPayServer.Controllers
|
||||||
public AccountController(
|
public AccountController(
|
||||||
UserManager<ApplicationUser> userManager,
|
UserManager<ApplicationUser> userManager,
|
||||||
RoleManager<IdentityRole> roleManager,
|
RoleManager<IdentityRole> roleManager,
|
||||||
StoreRepository storeRepository,
|
|
||||||
SignInManager<ApplicationUser> signInManager,
|
SignInManager<ApplicationUser> signInManager,
|
||||||
EmailSenderFactory emailSenderFactory,
|
|
||||||
SettingsRepository settingsRepository,
|
SettingsRepository settingsRepository,
|
||||||
Configuration.BTCPayServerOptions options,
|
Configuration.BTCPayServerOptions options,
|
||||||
BTCPayServerEnvironment btcPayServerEnvironment,
|
BTCPayServerEnvironment btcPayServerEnvironment,
|
||||||
U2FService u2FService,
|
U2FService u2FService,
|
||||||
EventAggregator eventAggregator)
|
EventAggregator eventAggregator)
|
||||||
{
|
{
|
||||||
this.storeRepository = storeRepository;
|
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_signInManager = signInManager;
|
_signInManager = signInManager;
|
||||||
_EmailSenderFactory = emailSenderFactory;
|
|
||||||
_RoleManager = roleManager;
|
_RoleManager = roleManager;
|
||||||
_SettingsRepository = settingsRepository;
|
_SettingsRepository = settingsRepository;
|
||||||
_Options = options;
|
_Options = options;
|
||||||
|
@ -71,7 +63,7 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<IActionResult> Login(string returnUrl = null)
|
public async Task<IActionResult> Login(string returnUrl = null, string email = null)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (User.Identity.IsAuthenticated && string.IsNullOrEmpty(returnUrl))
|
if (User.Identity.IsAuthenticated && string.IsNullOrEmpty(returnUrl))
|
||||||
|
@ -85,7 +77,10 @@ namespace BTCPayServer.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewData["ReturnUrl"] = returnUrl;
|
ViewData["ReturnUrl"] = returnUrl;
|
||||||
return View();
|
return View(new LoginViewModel()
|
||||||
|
{
|
||||||
|
Email = email
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -500,8 +495,30 @@ namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
throw new ApplicationException($"Unable to load user with ID '{userId}'.");
|
throw new ApplicationException($"Unable to load user with ID '{userId}'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await _userManager.ConfirmEmailAsync(user, code);
|
var result = await _userManager.ConfirmEmailAsync(user, code);
|
||||||
return View(result.Succeeded ? "ConfirmEmail" : "Error");
|
if (!await _userManager.HasPasswordAsync(user))
|
||||||
|
{
|
||||||
|
|
||||||
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
|
{
|
||||||
|
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||||
|
Message = "Your email has been confirmed but you still need to set your password."
|
||||||
|
});
|
||||||
|
return RedirectToAction("SetPassword", new { email = user.Email, code= await _userManager.GeneratePasswordResetTokenAsync(user)});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
|
{
|
||||||
|
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||||
|
Message = "Your email has been confirmed."
|
||||||
|
});
|
||||||
|
return RedirectToAction("Login", new {email = user.Email});
|
||||||
|
}
|
||||||
|
|
||||||
|
return View("Error");
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
|
@ -524,14 +541,10 @@ namespace BTCPayServer.Controllers
|
||||||
// Don't reveal that the user does not exist or is not confirmed
|
// Don't reveal that the user does not exist or is not confirmed
|
||||||
return RedirectToAction(nameof(ForgotPasswordConfirmation));
|
return RedirectToAction(nameof(ForgotPasswordConfirmation));
|
||||||
}
|
}
|
||||||
|
_eventAggregator.Publish(new UserPasswordResetRequestedEvent()
|
||||||
// For more information on how to enable account confirmation and password reset please
|
{
|
||||||
// visit https://go.microsoft.com/fwlink/?LinkID=532713
|
User = user, RequestUri = Request.GetAbsoluteRootUri()
|
||||||
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
|
});
|
||||||
var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme);
|
|
||||||
_EmailSenderFactory.GetEmailSender().SendEmail(model.Email, "Reset Password",
|
|
||||||
$"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
|
|
||||||
|
|
||||||
return RedirectToAction(nameof(ForgotPasswordConfirmation));
|
return RedirectToAction(nameof(ForgotPasswordConfirmation));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -548,20 +561,27 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public IActionResult ResetPassword(string code = null)
|
public async Task<IActionResult> SetPassword(string code = null, string userId = null, string email = null)
|
||||||
{
|
{
|
||||||
if (code == null)
|
if (code == null)
|
||||||
{
|
{
|
||||||
throw new ApplicationException("A code must be supplied for password reset.");
|
throw new ApplicationException("A code must be supplied for password reset.");
|
||||||
}
|
}
|
||||||
var model = new ResetPasswordViewModel { Code = code };
|
|
||||||
|
if (!string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
var user = await _userManager.FindByIdAsync(userId);
|
||||||
|
email = user?.Email;
|
||||||
|
}
|
||||||
|
|
||||||
|
var model = new SetPasswordViewModel {Code = code, Email = email, EmailSetInternally = !string.IsNullOrEmpty(email)};
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
|
public async Task<IActionResult> SetPassword(SetPasswordViewModel model)
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
|
@ -571,25 +591,23 @@ namespace BTCPayServer.Controllers
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
// Don't reveal that the user does not exist
|
// Don't reveal that the user does not exist
|
||||||
return RedirectToAction(nameof(ResetPasswordConfirmation));
|
return RedirectToAction(nameof(Login));
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
|
var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
|
||||||
if (result.Succeeded)
|
if (result.Succeeded)
|
||||||
{
|
{
|
||||||
return RedirectToAction(nameof(ResetPasswordConfirmation));
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
|
{
|
||||||
|
Severity = StatusMessageModel.StatusSeverity.Success, Message = "Password successfully set."
|
||||||
|
});
|
||||||
|
return RedirectToAction(nameof(Login));
|
||||||
}
|
}
|
||||||
|
|
||||||
AddErrors(result);
|
AddErrors(result);
|
||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[AllowAnonymous]
|
|
||||||
public IActionResult ResetPasswordConfirmation()
|
|
||||||
{
|
|
||||||
return View();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public IActionResult AccessDenied()
|
public IActionResult AccessDenied()
|
||||||
{
|
{
|
||||||
|
|
245
BTCPayServer/Controllers/ServerController.Users.cs
Normal file
245
BTCPayServer/Controllers/ServerController.Users.cs
Normal file
|
@ -0,0 +1,245 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Events;
|
||||||
|
using BTCPayServer.Models;
|
||||||
|
using BTCPayServer.Models.ServerViewModels;
|
||||||
|
using BTCPayServer.Storage.Services;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Controllers
|
||||||
|
{
|
||||||
|
public partial class ServerController
|
||||||
|
{
|
||||||
|
[Route("server/users")]
|
||||||
|
public IActionResult ListUsers(int skip = 0, int count = 50)
|
||||||
|
{
|
||||||
|
var users = new UsersViewModel();
|
||||||
|
users.Users = _UserManager.Users.Skip(skip).Take(count)
|
||||||
|
.Select(u => new UsersViewModel.UserViewModel
|
||||||
|
{
|
||||||
|
Name = u.UserName,
|
||||||
|
Email = u.Email,
|
||||||
|
Id = u.Id
|
||||||
|
}).ToList();
|
||||||
|
users.Skip = skip;
|
||||||
|
users.Count = count;
|
||||||
|
users.Total = _UserManager.Users.Count();
|
||||||
|
return View(users);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("server/users/{userId}")]
|
||||||
|
public new async Task<IActionResult> User(string userId)
|
||||||
|
{
|
||||||
|
var user = await _UserManager.FindByIdAsync(userId);
|
||||||
|
if (user == null)
|
||||||
|
return NotFound();
|
||||||
|
var roles = await _UserManager.GetRolesAsync(user);
|
||||||
|
var userVM = new UserViewModel();
|
||||||
|
userVM.Id = user.Id;
|
||||||
|
userVM.Email = user.Email;
|
||||||
|
userVM.IsAdmin = IsAdmin(roles);
|
||||||
|
return View(userVM);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static bool IsAdmin(IList<string> roles)
|
||||||
|
{
|
||||||
|
return roles.Contains(Roles.ServerAdmin, StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("server/users/{userId}")]
|
||||||
|
[HttpPost]
|
||||||
|
public new async Task<IActionResult> User(string userId, UserViewModel viewModel)
|
||||||
|
{
|
||||||
|
var user = await _UserManager.FindByIdAsync(userId);
|
||||||
|
if (user == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||||
|
var roles = await _UserManager.GetRolesAsync(user);
|
||||||
|
var wasAdmin = IsAdmin(roles);
|
||||||
|
if (!viewModel.IsAdmin && admins.Count == 1 && wasAdmin)
|
||||||
|
{
|
||||||
|
TempData[WellKnownTempData.ErrorMessage] = "This is the only Admin, so their role can't be removed until another Admin is added.";
|
||||||
|
return View(viewModel); // return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewModel.IsAdmin != wasAdmin)
|
||||||
|
{
|
||||||
|
if (viewModel.IsAdmin)
|
||||||
|
await _UserManager.AddToRoleAsync(user, Roles.ServerAdmin);
|
||||||
|
else
|
||||||
|
await _UserManager.RemoveFromRoleAsync(user, Roles.ServerAdmin);
|
||||||
|
|
||||||
|
TempData[WellKnownTempData.SuccessMessage] = "User successfully updated";
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(User), new { userId = userId });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("server/users/new")]
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult CreateUser()
|
||||||
|
{
|
||||||
|
ViewData["AllowIsAdmin"] = _Options.AllowAdminRegistration;
|
||||||
|
ViewData["AllowRequestEmailConfirmation"] = _cssThemeManager.Policies.RequiresConfirmedEmail;
|
||||||
|
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("server/users/new")]
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> CreateUser(RegisterFromAdminViewModel model)
|
||||||
|
{
|
||||||
|
ViewData["AllowIsAdmin"] = _Options.AllowAdminRegistration;
|
||||||
|
ViewData["AllowRequestEmailConfirmation"] = _cssThemeManager.Policies.RequiresConfirmedEmail;
|
||||||
|
|
||||||
|
if (ModelState.IsValid)
|
||||||
|
{
|
||||||
|
IdentityResult result;
|
||||||
|
var user = new ApplicationUser { UserName = model.Email, Email = model.Email, EmailConfirmed = model.EmailConfirmed, RequiresEmailConfirmation = _cssThemeManager.Policies.RequiresConfirmedEmail };
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(model.Password))
|
||||||
|
{
|
||||||
|
|
||||||
|
result = await _UserManager.CreateAsync(user, model.Password);
|
||||||
|
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
TempData[WellKnownTempData.SuccessMessage] = "Account created";
|
||||||
|
return RedirectToAction(nameof(ListUsers));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = await _UserManager.CreateAsync(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
if (model.IsAdmin && !(await _UserManager.AddToRoleAsync(user, Roles.ServerAdmin)).Succeeded)
|
||||||
|
model.IsAdmin = false;
|
||||||
|
|
||||||
|
var tcs = new TaskCompletionSource<Uri>();
|
||||||
|
|
||||||
|
_eventAggregator.Publish(new UserRegisteredEvent()
|
||||||
|
{
|
||||||
|
RequestUri = Request.GetAbsoluteRootUri(), User = user, Admin = model.IsAdmin is true, CallbackUrlGenerated = tcs
|
||||||
|
});
|
||||||
|
var callbackUrl = await tcs.Task;
|
||||||
|
|
||||||
|
if (user.RequiresEmailConfirmation && !user.EmailConfirmed)
|
||||||
|
{
|
||||||
|
|
||||||
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
|
{
|
||||||
|
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||||
|
AllowDismiss = false,
|
||||||
|
Html =
|
||||||
|
$"Account created without a set password. An email will be sent (if configured) to set the password.<br/> You may alternatively share this link with them: <a class='alert-link' href='{callbackUrl}'>{callbackUrl}</a>"
|
||||||
|
});
|
||||||
|
}else if (!await _UserManager.HasPasswordAsync(user))
|
||||||
|
{
|
||||||
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
|
{
|
||||||
|
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||||
|
AllowDismiss = false,
|
||||||
|
Html =
|
||||||
|
$"Account created without a set password. An email will be sent (if configured) to set the password.<br/> You may alternatively share this link with them: <a class='alert-link' href='{callbackUrl}'>{callbackUrl}</a>"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return RedirectToAction(nameof(ListUsers));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var error in result.Errors)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(string.Empty, error.Description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got this far, something failed, redisplay form
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("server/users/{userId}/delete")]
|
||||||
|
public async Task<IActionResult> DeleteUser(string userId)
|
||||||
|
{
|
||||||
|
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
|
||||||
|
if (user == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
var roles = await _UserManager.GetRolesAsync(user);
|
||||||
|
if (IsAdmin(roles))
|
||||||
|
{
|
||||||
|
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||||
|
if (admins.Count == 1)
|
||||||
|
{
|
||||||
|
// return
|
||||||
|
return View("Confirm", new ConfirmModel("Unable to Delete Last Admin",
|
||||||
|
"This is the last Admin, so it can't be removed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return View("Confirm", new ConfirmModel("Delete Admin " + user.Email,
|
||||||
|
"Are you sure you want to delete this Admin and delete all accounts, users and data associated with the server account?",
|
||||||
|
"Delete"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return View("Confirm", new ConfirmModel("Delete user " + user.Email,
|
||||||
|
"This user will be permanently deleted",
|
||||||
|
"Delete"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("server/users/{userId}/delete")]
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> DeleteUserPost(string userId)
|
||||||
|
{
|
||||||
|
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
|
||||||
|
if (user == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
var files = await _StoredFileRepository.GetFiles(new StoredFileRepository.FilesQuery()
|
||||||
|
{
|
||||||
|
UserIds = new[] { userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(files.Select(file => _FileService.RemoveFile(file.Id, userId)));
|
||||||
|
|
||||||
|
await _UserManager.DeleteAsync(user);
|
||||||
|
await _StoreRepository.CleanUnreachableStores();
|
||||||
|
TempData[WellKnownTempData.SuccessMessage] = "User deleted";
|
||||||
|
return RedirectToAction(nameof(ListUsers));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RegisterFromAdminViewModel
|
||||||
|
{
|
||||||
|
[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 (leave blank to generate invite-link)")]
|
||||||
|
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; }
|
||||||
|
|
||||||
|
[Display(Name = "Is administrator?")]
|
||||||
|
public bool IsAdmin { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Email confirmed?")]
|
||||||
|
public bool EmailConfirmed { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
|
@ -47,6 +48,8 @@ namespace BTCPayServer.Controllers
|
||||||
private readonly BTCPayServerOptions _Options;
|
private readonly BTCPayServerOptions _Options;
|
||||||
private readonly AppService _AppService;
|
private readonly AppService _AppService;
|
||||||
private readonly CheckConfigurationHostedService _sshState;
|
private readonly CheckConfigurationHostedService _sshState;
|
||||||
|
private readonly EventAggregator _eventAggregator;
|
||||||
|
private readonly CssThemeManager _cssThemeManager;
|
||||||
private readonly StoredFileRepository _StoredFileRepository;
|
private readonly StoredFileRepository _StoredFileRepository;
|
||||||
private readonly FileService _FileService;
|
private readonly FileService _FileService;
|
||||||
private readonly IEnumerable<IStorageProviderService> _StorageProviderServices;
|
private readonly IEnumerable<IStorageProviderService> _StorageProviderServices;
|
||||||
|
@ -63,7 +66,9 @@ namespace BTCPayServer.Controllers
|
||||||
TorServices torServices,
|
TorServices torServices,
|
||||||
StoreRepository storeRepository,
|
StoreRepository storeRepository,
|
||||||
AppService appService,
|
AppService appService,
|
||||||
CheckConfigurationHostedService sshState)
|
CheckConfigurationHostedService sshState,
|
||||||
|
EventAggregator eventAggregator,
|
||||||
|
CssThemeManager cssThemeManager)
|
||||||
{
|
{
|
||||||
_Options = options;
|
_Options = options;
|
||||||
_StoredFileRepository = storedFileRepository;
|
_StoredFileRepository = storedFileRepository;
|
||||||
|
@ -78,37 +83,8 @@ namespace BTCPayServer.Controllers
|
||||||
_torServices = torServices;
|
_torServices = torServices;
|
||||||
_AppService = appService;
|
_AppService = appService;
|
||||||
_sshState = sshState;
|
_sshState = sshState;
|
||||||
}
|
_eventAggregator = eventAggregator;
|
||||||
|
_cssThemeManager = cssThemeManager;
|
||||||
[Route("server/users")]
|
|
||||||
public IActionResult ListUsers(int skip = 0, int count = 50)
|
|
||||||
{
|
|
||||||
var users = new UsersViewModel();
|
|
||||||
users.Users = _UserManager.Users.Skip(skip).Take(count)
|
|
||||||
.Select(u => new UsersViewModel.UserViewModel
|
|
||||||
{
|
|
||||||
Name = u.UserName,
|
|
||||||
Email = u.Email,
|
|
||||||
Id = u.Id
|
|
||||||
}).ToList();
|
|
||||||
users.Skip = skip;
|
|
||||||
users.Count = count;
|
|
||||||
users.Total = _UserManager.Users.Count();
|
|
||||||
return View(users);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Route("server/users/{userId}")]
|
|
||||||
public new async Task<IActionResult> User(string userId)
|
|
||||||
{
|
|
||||||
var user = await _UserManager.FindByIdAsync(userId);
|
|
||||||
if (user == null)
|
|
||||||
return NotFound();
|
|
||||||
var roles = await _UserManager.GetRolesAsync(user);
|
|
||||||
var userVM = new UserViewModel();
|
|
||||||
userVM.Id = user.Id;
|
|
||||||
userVM.Email = user.Email;
|
|
||||||
userVM.IsAdmin = IsAdmin(roles);
|
|
||||||
return View(userVM);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route("server/maintenance")]
|
[Route("server/maintenance")]
|
||||||
|
@ -270,127 +246,7 @@ namespace BTCPayServer.Controllers
|
||||||
sshClient.Dispose();
|
sshClient.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsAdmin(IList<string> roles)
|
|
||||||
{
|
|
||||||
return roles.Contains(Roles.ServerAdmin, StringComparer.Ordinal);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Route("server/users/{userId}")]
|
|
||||||
[HttpPost]
|
|
||||||
public new async Task<IActionResult> User(string userId, UserViewModel viewModel)
|
|
||||||
{
|
|
||||||
var user = await _UserManager.FindByIdAsync(userId);
|
|
||||||
if (user == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
|
||||||
var roles = await _UserManager.GetRolesAsync(user);
|
|
||||||
var wasAdmin = IsAdmin(roles);
|
|
||||||
if (!viewModel.IsAdmin && admins.Count == 1 && wasAdmin)
|
|
||||||
{
|
|
||||||
TempData[WellKnownTempData.ErrorMessage] = "This is the only Admin, so their role can't be removed until another Admin is added.";
|
|
||||||
return View(viewModel); // return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewModel.IsAdmin != wasAdmin)
|
|
||||||
{
|
|
||||||
if (viewModel.IsAdmin)
|
|
||||||
await _UserManager.AddToRoleAsync(user, Roles.ServerAdmin);
|
|
||||||
else
|
|
||||||
await _UserManager.RemoveFromRoleAsync(user, Roles.ServerAdmin);
|
|
||||||
|
|
||||||
TempData[WellKnownTempData.SuccessMessage] = "User successfully updated";
|
|
||||||
}
|
|
||||||
|
|
||||||
return RedirectToAction(nameof(User), new { userId = userId });
|
|
||||||
}
|
|
||||||
|
|
||||||
[Route("server/users/new")]
|
|
||||||
[HttpGet]
|
|
||||||
public IActionResult CreateUser()
|
|
||||||
{
|
|
||||||
ViewData["AllowIsAdmin"] = _Options.AllowAdminRegistration;
|
|
||||||
|
|
||||||
return View();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Route("server/users/new")]
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<IActionResult> CreateUser(RegisterViewModel model)
|
|
||||||
{
|
|
||||||
ViewData["AllowIsAdmin"] = _Options.AllowAdminRegistration;
|
|
||||||
|
|
||||||
if (ModelState.IsValid)
|
|
||||||
{
|
|
||||||
var user = new ApplicationUser { UserName = model.Email, Email = model.Email, RequiresEmailConfirmation = false };
|
|
||||||
var result = await _UserManager.CreateAsync(user, model.Password);
|
|
||||||
if (result.Succeeded)
|
|
||||||
{
|
|
||||||
TempData[WellKnownTempData.SuccessMessage] = "Account created";
|
|
||||||
return RedirectToAction(nameof(ListUsers));
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var error in result.Errors)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError(string.Empty, error.Description);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we got this far, something failed, redisplay form
|
|
||||||
return View(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Route("server/users/{userId}/delete")]
|
|
||||||
public async Task<IActionResult> DeleteUser(string userId)
|
|
||||||
{
|
|
||||||
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
|
|
||||||
if (user == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
var roles = await _UserManager.GetRolesAsync(user);
|
|
||||||
if (IsAdmin(roles))
|
|
||||||
{
|
|
||||||
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
|
||||||
if (admins.Count == 1)
|
|
||||||
{
|
|
||||||
// return
|
|
||||||
return View("Confirm", new ConfirmModel("Unable to Delete Last Admin",
|
|
||||||
"This is the last Admin, so it can't be removed"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return View("Confirm", new ConfirmModel("Delete Admin " + user.Email,
|
|
||||||
"Are you sure you want to delete this Admin and delete all accounts, users and data associated with the server account?",
|
|
||||||
"Delete"));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return View("Confirm", new ConfirmModel("Delete user " + user.Email,
|
|
||||||
"This user will be permanently deleted",
|
|
||||||
"Delete"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Route("server/users/{userId}/delete")]
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<IActionResult> DeleteUserPost(string userId)
|
|
||||||
{
|
|
||||||
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
|
|
||||||
if (user == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
var files = await _StoredFileRepository.GetFiles(new StoredFileRepository.FilesQuery()
|
|
||||||
{
|
|
||||||
UserIds = new[] { userId },
|
|
||||||
});
|
|
||||||
|
|
||||||
await Task.WhenAll(files.Select(file => _FileService.RemoveFile(file.Id, userId)));
|
|
||||||
|
|
||||||
await _UserManager.DeleteAsync(user);
|
|
||||||
await _StoreRepository.CleanUnreachableStores();
|
|
||||||
TempData[WellKnownTempData.SuccessMessage] = "User deleted";
|
|
||||||
return RedirectToAction(nameof(ListUsers));
|
|
||||||
}
|
|
||||||
public IHttpClientFactory HttpClientFactory { get; }
|
public IHttpClientFactory HttpClientFactory { get; }
|
||||||
|
|
||||||
[Route("server/policies")]
|
[Route("server/policies")]
|
||||||
|
|
13
BTCPayServer/Events/UserPasswordResetRequestedEvent.cs
Normal file
13
BTCPayServer/Events/UserPasswordResetRequestedEvent.cs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Events
|
||||||
|
{
|
||||||
|
public class UserPasswordResetRequestedEvent
|
||||||
|
{
|
||||||
|
public ApplicationUser User { get; set; }
|
||||||
|
public Uri RequestUri { get; set; }
|
||||||
|
public TaskCompletionSource<Uri> CallbackUrlGenerated;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
|
||||||
namespace BTCPayServer.Events
|
namespace BTCPayServer.Events
|
||||||
|
@ -8,5 +9,7 @@ namespace BTCPayServer.Events
|
||||||
public ApplicationUser User { get; set; }
|
public ApplicationUser User { get; set; }
|
||||||
public bool Admin { get; set; }
|
public bool Admin { get; set; }
|
||||||
public Uri RequestUri { get; set; }
|
public Uri RequestUri { get; set; }
|
||||||
|
|
||||||
|
public TaskCompletionSource<Uri> CallbackUrlGenerated;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,5 +10,12 @@ namespace BTCPayServer.Services
|
||||||
emailSender.SendEmail(email, "Confirm your email",
|
emailSender.SendEmail(email, "Confirm your email",
|
||||||
$"Please confirm your account by clicking this link: <a href='{HtmlEncoder.Default.Encode(link)}'>link</a>");
|
$"Please confirm your account by clicking this link: <a href='{HtmlEncoder.Default.Encode(link)}'>link</a>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void SendSetPasswordConfirmation(this IEmailSender emailSender, string email, string link, bool newPassword)
|
||||||
|
{
|
||||||
|
emailSender.SendEmail(email,
|
||||||
|
$"{(newPassword ? "Set" : "Reset")} Password",
|
||||||
|
$"Please {(newPassword ? "set" : "reset")} your password by clicking here: <a href='{link}'>link</a>");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,13 +15,16 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
new { userId, code }, scheme, host, pathbase);
|
new { userId, code }, scheme, host, pathbase);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string ResetPasswordCallbackLink(this IUrlHelper urlHelper, string userId, string code, string scheme)
|
public static string ResetPasswordCallbackLink(this LinkGenerator urlHelper, string userId, string code, string scheme, HostString host, string pathbase)
|
||||||
{
|
{
|
||||||
return urlHelper.Action(
|
return urlHelper.GetUriByAction(
|
||||||
action: nameof(AccountController.ResetPassword),
|
action: nameof(AccountController.SetPassword),
|
||||||
controller: "Account",
|
controller: "Account",
|
||||||
values: new { userId, code },
|
values: new { userId, code },
|
||||||
protocol: scheme);
|
scheme: scheme,
|
||||||
|
host:host,
|
||||||
|
pathBase: pathbase
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string PaymentRequestLink(this LinkGenerator urlHelper, string paymentRequestId, string scheme, HostString host, string pathbase)
|
public static string PaymentRequestLink(this LinkGenerator urlHelper, string paymentRequestId, string scheme, HostString host, string pathbase)
|
||||||
|
|
|
@ -54,34 +54,32 @@ namespace BTCPayServer.HostedServices
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _creativeStartUri;
|
private string _creativeStartUri;
|
||||||
|
private PoliciesSettings _policies = new PoliciesSettings();
|
||||||
|
|
||||||
|
public PoliciesSettings Policies { get { return _policies; } }
|
||||||
public string CreativeStartUri
|
public string CreativeStartUri
|
||||||
{
|
{
|
||||||
get { return _creativeStartUri; }
|
get { return _creativeStartUri; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public bool ShowRegister { get; set; }
|
public bool ShowRegister { get { return !_policies.LockSubscription; } }
|
||||||
public bool DiscourageSearchEngines { get; set; }
|
public bool DiscourageSearchEngines { get { return _policies.DiscourageSearchEngines; } }
|
||||||
|
public AppType? RootAppType { get { return _policies.RootAppType; } }
|
||||||
public AppType? RootAppType { get; set; }
|
public string RootAppId { get { return _policies.RootAppId; } }
|
||||||
public string RootAppId { get; set; }
|
|
||||||
|
|
||||||
public bool FirstRun { get; set; }
|
public bool FirstRun { get; set; }
|
||||||
|
|
||||||
public List<PoliciesSettings.DomainToAppMappingItem> DomainToAppMapping { get; set; } = new List<PoliciesSettings.DomainToAppMappingItem>();
|
public List<PoliciesSettings.DomainToAppMappingItem> DomainToAppMapping { get { return _policies.DomainToAppMapping; } }
|
||||||
|
|
||||||
internal void Update(PoliciesSettings data)
|
internal void Update(PoliciesSettings data)
|
||||||
{
|
{
|
||||||
ShowRegister = !data.LockSubscription;
|
_policies = data;
|
||||||
DiscourageSearchEngines = data.DiscourageSearchEngines;
|
|
||||||
|
|
||||||
RootAppType = data.RootAppType;
|
|
||||||
RootAppId = data.RootAppId;
|
|
||||||
DomainToAppMapping = data.DomainToAppMapping;
|
|
||||||
AllowLightningInternalNodeForAll = data.AllowLightningInternalNodeForAll;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool AllowLightningInternalNodeForAll { get; set; }
|
public bool AllowLightningInternalNodeForAll { get { return _policies.AllowLightningInternalNodeForAll; } }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ContentSecurityPolicyCssThemeManager : Attribute, IActionFilter, IOrderedFilter
|
public class ContentSecurityPolicyCssThemeManager : Attribute, IActionFilter, IOrderedFilter
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
@ -30,23 +31,60 @@ namespace BTCPayServer.HostedServices
|
||||||
protected override void SubscribeToEvents()
|
protected override void SubscribeToEvents()
|
||||||
{
|
{
|
||||||
Subscribe<UserRegisteredEvent>();
|
Subscribe<UserRegisteredEvent>();
|
||||||
|
Subscribe<UserPasswordResetRequestedEvent>();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
string code;
|
||||||
|
string callbackUrl;
|
||||||
|
UserPasswordResetRequestedEvent userPasswordResetRequestedEvent;
|
||||||
switch (evt)
|
switch (evt)
|
||||||
{
|
{
|
||||||
case UserRegisteredEvent userRegisteredEvent:
|
case UserRegisteredEvent userRegisteredEvent:
|
||||||
Logs.PayServer.LogInformation($"A new user just registered {userRegisteredEvent.User.Email} {(userRegisteredEvent.Admin ? "(admin)" : "")}");
|
Logs.PayServer.LogInformation(
|
||||||
|
$"A new user just registered {userRegisteredEvent.User.Email} {(userRegisteredEvent.Admin ? "(admin)" : "")}");
|
||||||
if (!userRegisteredEvent.User.EmailConfirmed && userRegisteredEvent.User.RequiresEmailConfirmation)
|
if (!userRegisteredEvent.User.EmailConfirmed && userRegisteredEvent.User.RequiresEmailConfirmation)
|
||||||
{
|
{
|
||||||
|
code = await _userManager.GenerateEmailConfirmationTokenAsync(userRegisteredEvent.User);
|
||||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(userRegisteredEvent.User);
|
callbackUrl = _generator.EmailConfirmationLink(userRegisteredEvent.User.Id, code,
|
||||||
var callbackUrl = _generator.EmailConfirmationLink(userRegisteredEvent.User.Id, code, userRegisteredEvent.RequestUri.Scheme, new HostString(userRegisteredEvent.RequestUri.Host, userRegisteredEvent.RequestUri.Port), userRegisteredEvent.RequestUri.PathAndQuery);
|
userRegisteredEvent.RequestUri.Scheme,
|
||||||
|
new HostString(userRegisteredEvent.RequestUri.Host, userRegisteredEvent.RequestUri.Port),
|
||||||
|
userRegisteredEvent.RequestUri.PathAndQuery);
|
||||||
|
userRegisteredEvent.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
||||||
_emailSenderFactory.GetEmailSender()
|
_emailSenderFactory.GetEmailSender()
|
||||||
.SendEmailConfirmation(userRegisteredEvent.User.Email, callbackUrl);
|
.SendEmailConfirmation(userRegisteredEvent.User.Email, callbackUrl);
|
||||||
}
|
}
|
||||||
|
else if (!await _userManager.HasPasswordAsync(userRegisteredEvent.User))
|
||||||
|
{
|
||||||
|
userPasswordResetRequestedEvent = new UserPasswordResetRequestedEvent()
|
||||||
|
{
|
||||||
|
CallbackUrlGenerated = userRegisteredEvent.CallbackUrlGenerated,
|
||||||
|
User = userRegisteredEvent.User,
|
||||||
|
RequestUri = userRegisteredEvent.RequestUri
|
||||||
|
};
|
||||||
|
goto passwordSetter;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
userRegisteredEvent.CallbackUrlGenerated?.SetResult(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case UserPasswordResetRequestedEvent userPasswordResetRequestedEvent2:
|
||||||
|
userPasswordResetRequestedEvent = userPasswordResetRequestedEvent2;
|
||||||
|
passwordSetter:
|
||||||
|
code = await _userManager.GeneratePasswordResetTokenAsync(userPasswordResetRequestedEvent.User);
|
||||||
|
var newPassword = await _userManager.HasPasswordAsync(userPasswordResetRequestedEvent.User);
|
||||||
|
callbackUrl = _generator.ResetPasswordCallbackLink(userPasswordResetRequestedEvent.User.Id, code,
|
||||||
|
userPasswordResetRequestedEvent.RequestUri.Scheme,
|
||||||
|
new HostString(userPasswordResetRequestedEvent.RequestUri.Host,
|
||||||
|
userPasswordResetRequestedEvent.RequestUri.Port),
|
||||||
|
userPasswordResetRequestedEvent.RequestUri.PathAndQuery);
|
||||||
|
userPasswordResetRequestedEvent.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
||||||
|
_emailSenderFactory.GetEmailSender()
|
||||||
|
.SendSetPasswordConfirmation(userPasswordResetRequestedEvent.User.Email, callbackUrl,
|
||||||
|
newPassword);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,11 @@ using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.AccountViewModels
|
namespace BTCPayServer.Models.AccountViewModels
|
||||||
{
|
{
|
||||||
public class ResetPasswordViewModel
|
public class SetPasswordViewModel
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
[EmailAddress]
|
[EmailAddress]
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||||
[DataType(DataType.Password)]
|
[DataType(DataType.Password)]
|
||||||
|
@ -19,5 +18,6 @@ namespace BTCPayServer.Models.AccountViewModels
|
||||||
public string ConfirmPassword { get; set; }
|
public string ConfirmPassword { get; set; }
|
||||||
|
|
||||||
public string Code { get; set; }
|
public string Code { get; set; }
|
||||||
|
public bool EmailSetInternally { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,14 +0,0 @@
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Confirm email";
|
|
||||||
}
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<div class="container">
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12 text-center">
|
|
||||||
<partial name="_StatusMessage" model="@("Thank you for confirming your email.")" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
|
@ -1,45 +0,0 @@
|
||||||
@model ResetPasswordViewModel
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Reset password";
|
|
||||||
}
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<div class="container">
|
|
||||||
@if (TempData.HasStatusMessage())
|
|
||||||
{
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12 text-center">
|
|
||||||
<partial name="_StatusMessage" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<form method="post">
|
|
||||||
<div asp-validation-summary="All" class="text-danger"></div>
|
|
||||||
<input asp-for="Code" type="hidden" />
|
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="Email"></label>
|
|
||||||
<input asp-for="Email" class="form-control" />
|
|
||||||
<span asp-validation-for="Email" class="text-danger"></span>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="Password"></label>
|
|
||||||
<input asp-for="Password" class="form-control" />
|
|
||||||
<span asp-validation-for="Password" class="text-danger"></span>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="ConfirmPassword"></label>
|
|
||||||
<input asp-for="ConfirmPassword" class="form-control" />
|
|
||||||
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">Reset</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
@section Scripts {
|
|
||||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Reset password confirmation";
|
|
||||||
}
|
|
||||||
|
|
||||||
<h2>@ViewData["Title"]</h2>
|
|
||||||
<section>
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12 text-center">
|
|
||||||
Your password has been reset. Please <a asp-action="Login">click here to log in</a>.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
76
BTCPayServer/Views/Account/SetPassword.cshtml
Normal file
76
BTCPayServer/Views/Account/SetPassword.cshtml
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
@model BTCPayServer.Models.AccountViewModels.SetPasswordViewModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Reset password";
|
||||||
|
|
||||||
|
Layout = "_LayoutSimple";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="row justify-content-center mb-2">
|
||||||
|
<div class="col text-center">
|
||||||
|
<a asp-controller="Home" asp-action="Index">
|
||||||
|
<img src="~/img/btcpay-logo.svg" alt="BTCPay Server" class="mb-4" height="70" asp-append-version="true"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<h1 class="h2 mb-3">Welcome to your BTCPay Server</h1>
|
||||||
|
|
||||||
|
@if (TempData.HasStatusMessage())
|
||||||
|
{
|
||||||
|
<partial name="_StatusMessage"/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center mb-5">
|
||||||
|
<div class="col account-form">
|
||||||
|
<div class="modal-content border-0 p-3">
|
||||||
|
<div class="modal-header align-items-center border-0 py-2">
|
||||||
|
<h4 class="modal-title">Set Password</h4>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<form method="post" asp-action="SetPassword">
|
||||||
|
<div asp-validation-summary="All" class="text-danger"></div>
|
||||||
|
<input asp-for="Code" type="hidden"/>
|
||||||
|
<input asp-for="EmailSetInternally" type="hidden"/>
|
||||||
|
@if (Model.EmailSetInternally)
|
||||||
|
{
|
||||||
|
<input asp-for="Email" type="hidden"/>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Email"></label>
|
||||||
|
<input type="text" disabled value="@Model.Email" class="form-control"/>
|
||||||
|
<span asp-validation-for="Email" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Email"></label>
|
||||||
|
<input asp-for="Email" value="@Model.Email" class="form-control"/>
|
||||||
|
<span asp-validation-for="Email" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Password"></label>
|
||||||
|
<input asp-for="Password" class="form-control"/>
|
||||||
|
<span asp-validation-for="Password" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="ConfirmPassword"></label>
|
||||||
|
<input asp-for="ConfirmPassword" class="form-control"/>
|
||||||
|
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-block btn-lg" id="SetPassword">Set password</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center mt-5">
|
||||||
|
<div class="col">
|
||||||
|
<partial name="_BTCPaySupporters"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,4 +1,4 @@
|
||||||
@model SetPasswordViewModel
|
@model BTCPayServer.Models.ManageViewModels.SetPasswordViewModel
|
||||||
@{
|
@{
|
||||||
ViewData.SetActivePageAndTitle(ManageNavPages.ChangePassword, "Set password");
|
ViewData.SetActivePageAndTitle(ManageNavPages.ChangePassword, "Set password");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@model RegisterViewModel
|
@model BTCPayServer.Controllers.RegisterFromAdminViewModel
|
||||||
@{
|
@{
|
||||||
ViewData.SetActivePageAndTitle(ServerNavPages.Users, $"Users - Create account");
|
ViewData.SetActivePageAndTitle(ServerNavPages.Users, $"Users - Create account");
|
||||||
}
|
}
|
||||||
|
@ -18,13 +18,13 @@
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="Password"></label>
|
<label asp-for="Password"></label>
|
||||||
<input asp-for="Password" required="required" class="form-control"/>
|
<input asp-for="Password" class="form-control"/>
|
||||||
<span asp-validation-for="Password" class="text-danger"></span>
|
<span asp-validation-for="Password" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="ConfirmPassword"></label>
|
<label asp-for="ConfirmPassword"></label>
|
||||||
<input asp-for="ConfirmPassword" required="required" class="form-control"/>
|
<input asp-for="ConfirmPassword" class="form-control"/>
|
||||||
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
|
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -36,8 +36,18 @@
|
||||||
<span asp-validation-for="IsAdmin" class="text-danger"></span>
|
<span asp-validation-for="IsAdmin" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary" name="command" value="Save">Create account</button>
|
@if (ViewData["AllowRequestEmailConfirmation"] is true)
|
||||||
|
{
|
||||||
|
<div class="form-group form-check">
|
||||||
|
<input asp-for="EmailConfirmed" type="checkbox" class="form-check-input"/>
|
||||||
|
<label asp-for="EmailConfirmed" class="form-check-label"></label>
|
||||||
|
<span asp-validation-for="EmailConfirmed" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<button id="Save" type="submit" class="btn btn-primary" name="command" value="Save">Create account</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<div class="col-lg-9 col-xl-8">
|
<div class="col-lg-9 col-xl-8">
|
||||||
<span>Total Users: @Model.Total</span>
|
<span>Total Users: @Model.Total</span>
|
||||||
<span class="pull-right">
|
<span class="pull-right">
|
||||||
<a asp-action="CreateUser" class="btn btn-primary" role="button">
|
<a asp-action="CreateUser" class="btn btn-primary" role="button" id="CreateUser">
|
||||||
<span class="fa fa-plus"></span> Add User
|
<span class="fa fa-plus"></span> Add User
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<div class="nav flex-column nav-pills mb-4">
|
<div class="nav flex-column nav-pills mb-4">
|
||||||
<a asp-controller="Server" class="nav-link @ViewData.IsActivePage(ServerNavPages.Users)" asp-action="ListUsers">Users</a>
|
<a asp-controller="Server" id="Server-@ServerNavPages.Users" class="nav-link @ViewData.IsActivePage(ServerNavPages.Users)" asp-action="ListUsers">Users</a>
|
||||||
<a asp-controller="Server" class="nav-link @ViewData.IsActivePage(ServerNavPages.Emails)" asp-action="Emails">Email server</a>
|
<a asp-controller="Server" id="Server-@ServerNavPages.Emails" class="nav-link @ViewData.IsActivePage(ServerNavPages.Emails)" asp-action="Emails">Email server</a>
|
||||||
<a asp-controller="Server" class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</a>
|
<a asp-controller="Server" id="Server-@ServerNavPages.Policies" class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</a>
|
||||||
<a asp-controller="Server" class="nav-link @ViewData.IsActivePage(ServerNavPages.Services)" asp-action="Services">Services</a>
|
<a asp-controller="Server" id="Server-@ServerNavPages.Services" class="nav-link @ViewData.IsActivePage(ServerNavPages.Services)" asp-action="Services">Services</a>
|
||||||
<a asp-controller="Server" class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
|
<a asp-controller="Server" id="Server-@ServerNavPages.Theme" class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
|
||||||
<a asp-controller="Server" class="nav-link @ViewData.IsActivePage(ServerNavPages.Maintenance)" asp-action="Maintenance">Maintenance</a>
|
<a asp-controller="Server" id="Server-@ServerNavPages.Maintenance" class="nav-link @ViewData.IsActivePage(ServerNavPages.Maintenance)" asp-action="Maintenance">Maintenance</a>
|
||||||
<a asp-controller="Server" class="nav-link @ViewData.IsActivePage(ServerNavPages.Logs)" asp-action="LogsView">Logs</a>
|
<a asp-controller="Server" id="Server-@ServerNavPages.Logs" class="nav-link @ViewData.IsActivePage(ServerNavPages.Logs)" asp-action="LogsView">Logs</a>
|
||||||
<a asp-controller="Server" class="nav-link @ViewData.IsActivePage(ServerNavPages.Files)" asp-action="Files">Files</a>
|
<a asp-controller="Server" id="Server-@ServerNavPages.Files" class="nav-link @ViewData.IsActivePage(ServerNavPages.Files)" asp-action="Files">Files</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Reference in a new issue