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.Tests.Logging;
|
||||
using BTCPayServer.Views.Manage;
|
||||
using BTCPayServer.Views.Server;
|
||||
using BTCPayServer.Views.Stores;
|
||||
using BTCPayServer.Views.Wallets;
|
||||
using NBitcoin;
|
||||
|
@ -384,6 +385,16 @@ namespace BTCPayServer.Tests
|
|||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
GoToInvoices();
|
||||
|
|
|
@ -8,6 +8,7 @@ using BTCPayServer.Data;
|
|||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Views.Server;
|
||||
using BTCPayServer.Views.Wallets;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
|
@ -88,7 +89,7 @@ namespace BTCPayServer.Tests
|
|||
await s.StartAsync();
|
||||
//Register & Log Out
|
||||
var email = s.RegisterNewUser();
|
||||
s.Driver.FindElement(By.Id("Logout")).Click();
|
||||
s.Logout();
|
||||
s.Driver.AssertNoError();
|
||||
Assert.Contains("Account/Login", s.Driver.Url);
|
||||
// Should show the Tor address
|
||||
|
@ -129,7 +130,32 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.Id("MySettings")).Click();
|
||||
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.Security;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.U2F;
|
||||
using BTCPayServer.U2F.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
@ -28,8 +26,6 @@ namespace BTCPayServer.Controllers
|
|||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||
private readonly EmailSenderFactory _EmailSenderFactory;
|
||||
readonly StoreRepository storeRepository;
|
||||
readonly RoleManager<IdentityRole> _RoleManager;
|
||||
readonly SettingsRepository _SettingsRepository;
|
||||
readonly Configuration.BTCPayServerOptions _Options;
|
||||
|
@ -41,19 +37,15 @@ namespace BTCPayServer.Controllers
|
|||
public AccountController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
RoleManager<IdentityRole> roleManager,
|
||||
StoreRepository storeRepository,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
EmailSenderFactory emailSenderFactory,
|
||||
SettingsRepository settingsRepository,
|
||||
Configuration.BTCPayServerOptions options,
|
||||
BTCPayServerEnvironment btcPayServerEnvironment,
|
||||
U2FService u2FService,
|
||||
EventAggregator eventAggregator)
|
||||
{
|
||||
this.storeRepository = storeRepository;
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_EmailSenderFactory = emailSenderFactory;
|
||||
_RoleManager = roleManager;
|
||||
_SettingsRepository = settingsRepository;
|
||||
_Options = options;
|
||||
|
@ -71,7 +63,7 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
[HttpGet]
|
||||
[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))
|
||||
|
@ -85,7 +77,10 @@ namespace BTCPayServer.Controllers
|
|||
}
|
||||
|
||||
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}'.");
|
||||
}
|
||||
|
||||
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]
|
||||
|
@ -524,14 +541,10 @@ namespace BTCPayServer.Controllers
|
|||
// Don't reveal that the user does not exist or is not confirmed
|
||||
return RedirectToAction(nameof(ForgotPasswordConfirmation));
|
||||
}
|
||||
|
||||
// For more information on how to enable account confirmation and password reset please
|
||||
// visit https://go.microsoft.com/fwlink/?LinkID=532713
|
||||
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>");
|
||||
|
||||
_eventAggregator.Publish(new UserPasswordResetRequestedEvent()
|
||||
{
|
||||
User = user, RequestUri = Request.GetAbsoluteRootUri()
|
||||
});
|
||||
return RedirectToAction(nameof(ForgotPasswordConfirmation));
|
||||
}
|
||||
|
||||
|
@ -548,20 +561,27 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult ResetPassword(string code = null)
|
||||
public async Task<IActionResult> SetPassword(string code = null, string userId = null, string email = null)
|
||||
{
|
||||
if (code == null)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
|
||||
public async Task<IActionResult> SetPassword(SetPasswordViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
|
@ -571,25 +591,23 @@ namespace BTCPayServer.Controllers
|
|||
if (user == null)
|
||||
{
|
||||
// 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);
|
||||
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);
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult ResetPasswordConfirmation()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
|
||||
[HttpGet]
|
||||
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.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
|
@ -47,6 +48,8 @@ namespace BTCPayServer.Controllers
|
|||
private readonly BTCPayServerOptions _Options;
|
||||
private readonly AppService _AppService;
|
||||
private readonly CheckConfigurationHostedService _sshState;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly CssThemeManager _cssThemeManager;
|
||||
private readonly StoredFileRepository _StoredFileRepository;
|
||||
private readonly FileService _FileService;
|
||||
private readonly IEnumerable<IStorageProviderService> _StorageProviderServices;
|
||||
|
@ -63,7 +66,9 @@ namespace BTCPayServer.Controllers
|
|||
TorServices torServices,
|
||||
StoreRepository storeRepository,
|
||||
AppService appService,
|
||||
CheckConfigurationHostedService sshState)
|
||||
CheckConfigurationHostedService sshState,
|
||||
EventAggregator eventAggregator,
|
||||
CssThemeManager cssThemeManager)
|
||||
{
|
||||
_Options = options;
|
||||
_StoredFileRepository = storedFileRepository;
|
||||
|
@ -78,37 +83,8 @@ namespace BTCPayServer.Controllers
|
|||
_torServices = torServices;
|
||||
_AppService = appService;
|
||||
_sshState = sshState;
|
||||
}
|
||||
|
||||
[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);
|
||||
_eventAggregator = eventAggregator;
|
||||
_cssThemeManager = cssThemeManager;
|
||||
}
|
||||
|
||||
[Route("server/maintenance")]
|
||||
|
@ -271,126 +247,6 @@ namespace BTCPayServer.Controllers
|
|||
}
|
||||
}
|
||||
|
||||
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; }
|
||||
|
||||
[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.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
|
@ -8,5 +9,7 @@ namespace BTCPayServer.Events
|
|||
public ApplicationUser User { get; set; }
|
||||
public bool Admin { get; set; }
|
||||
public Uri RequestUri { get; set; }
|
||||
|
||||
public TaskCompletionSource<Uri> CallbackUrlGenerated;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,5 +10,12 @@ namespace BTCPayServer.Services
|
|||
emailSender.SendEmail(email, "Confirm your email",
|
||||
$"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);
|
||||
}
|
||||
|
||||
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(
|
||||
action: nameof(AccountController.ResetPassword),
|
||||
return urlHelper.GetUriByAction(
|
||||
action: nameof(AccountController.SetPassword),
|
||||
controller: "Account",
|
||||
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)
|
||||
|
|
|
@ -54,34 +54,32 @@ namespace BTCPayServer.HostedServices
|
|||
}
|
||||
|
||||
private string _creativeStartUri;
|
||||
private PoliciesSettings _policies = new PoliciesSettings();
|
||||
|
||||
public PoliciesSettings Policies { get { return _policies; } }
|
||||
public string CreativeStartUri
|
||||
{
|
||||
get { return _creativeStartUri; }
|
||||
}
|
||||
|
||||
|
||||
public bool ShowRegister { get; set; }
|
||||
public bool DiscourageSearchEngines { get; set; }
|
||||
|
||||
public AppType? RootAppType { get; set; }
|
||||
public string RootAppId { get; set; }
|
||||
public bool ShowRegister { get { return !_policies.LockSubscription; } }
|
||||
public bool DiscourageSearchEngines { get { return _policies.DiscourageSearchEngines; } }
|
||||
public AppType? RootAppType { get { return _policies.RootAppType; } }
|
||||
public string RootAppId { get { return _policies.RootAppId; } }
|
||||
|
||||
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)
|
||||
{
|
||||
ShowRegister = !data.LockSubscription;
|
||||
DiscourageSearchEngines = data.DiscourageSearchEngines;
|
||||
_policies = data;
|
||||
|
||||
|
||||
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
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
|
@ -30,23 +31,60 @@ namespace BTCPayServer.HostedServices
|
|||
protected override void SubscribeToEvents()
|
||||
{
|
||||
Subscribe<UserRegisteredEvent>();
|
||||
Subscribe<UserPasswordResetRequestedEvent>();
|
||||
}
|
||||
|
||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||
{
|
||||
string code;
|
||||
string callbackUrl;
|
||||
UserPasswordResetRequestedEvent userPasswordResetRequestedEvent;
|
||||
switch (evt)
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(userRegisteredEvent.User);
|
||||
var callbackUrl = _generator.EmailConfirmationLink(userRegisteredEvent.User.Id, code, userRegisteredEvent.RequestUri.Scheme, new HostString(userRegisteredEvent.RequestUri.Host, userRegisteredEvent.RequestUri.Port), userRegisteredEvent.RequestUri.PathAndQuery);
|
||||
|
||||
code = await _userManager.GenerateEmailConfirmationTokenAsync(userRegisteredEvent.User);
|
||||
callbackUrl = _generator.EmailConfirmationLink(userRegisteredEvent.User.Id, code,
|
||||
userRegisteredEvent.RequestUri.Scheme,
|
||||
new HostString(userRegisteredEvent.RequestUri.Host, userRegisteredEvent.RequestUri.Port),
|
||||
userRegisteredEvent.RequestUri.PathAndQuery);
|
||||
userRegisteredEvent.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
||||
_emailSenderFactory.GetEmailSender()
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,11 @@ using System.ComponentModel.DataAnnotations;
|
|||
|
||||
namespace BTCPayServer.Models.AccountViewModels
|
||||
{
|
||||
public class ResetPasswordViewModel
|
||||
public class SetPasswordViewModel
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Password)]
|
||||
|
@ -19,5 +18,6 @@ namespace BTCPayServer.Models.AccountViewModels
|
|||
public string ConfirmPassword { 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");
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@model RegisterViewModel
|
||||
@model BTCPayServer.Controllers.RegisterFromAdminViewModel
|
||||
@{
|
||||
ViewData.SetActivePageAndTitle(ServerNavPages.Users, $"Users - Create account");
|
||||
}
|
||||
|
@ -18,13 +18,13 @@
|
|||
|
||||
<div class="form-group">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
@ -37,7 +37,17 @@
|
|||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<div class="col-lg-9 col-xl-8">
|
||||
<span>Total Users: @Model.Total</span>
|
||||
<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
|
||||
</a>
|
||||
</span>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<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" 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" 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" 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" class="nav-link @ViewData.IsActivePage(ServerNavPages.Files)" asp-action="Files">Files</a>
|
||||
<div class="nav flex-column nav-pills mb-4">
|
||||
<a asp-controller="Server" id="Server-@ServerNavPages.Users" class="nav-link @ViewData.IsActivePage(ServerNavPages.Users)" asp-action="ListUsers">Users</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" id="Server-@ServerNavPages.Policies" class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</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" id="Server-@ServerNavPages.Theme" class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</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" id="Server-@ServerNavPages.Logs" class="nav-link @ViewData.IsActivePage(ServerNavPages.Logs)" asp-action="LogsView">Logs</a>
|
||||
<a asp-controller="Server" id="Server-@ServerNavPages.Files" class="nav-link @ViewData.IsActivePage(ServerNavPages.Files)" asp-action="Files">Files</a>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Reference in a new issue