btcpayserver/BTCPayServer/Controllers/ServerController.Users.cs
Kukks 371b33a2e1 Allow admins to invite new users
* This refactors the email sending so that all the logic related to users and emails are now contained in one location.
* The Reset password screen has been updated from its ugly plain self to use the same layout as the login.
* An admin can now create a new account without specifying a password. A link is generated that can be given to the intended user to configure the password. If emails are configured, it also sends an email
* An admin can now create accounts that still require the user to verify their if the setting is enabled from the server settings. A link is generated that can be given to the intended user to configure the password. If emails are configured, it also sends an email.
* The above features can be used in conjunction: An email will have to verify their email through a link. Once verified, the user is redirected to setting the password.
* When an email has been verified OR a password has been set, users are now redirected to the login page with the email filled in and a success status message shown instead of a dedicated thank you page.
2020-09-16 08:54:24 +02:00

246 lines
9.7 KiB
C#

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; }
}
}