mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 05:54:26 +01:00
Admins can approve registered users (#5647)
* Users list: Cleanups * Policies: Flip registration settings * Policies: Add RequireUserApproval setting * Add approval to user * Require approval on login and for API key * API handling * AccountController cleanups * Test fix * Apply suggestions from code review Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com> * Add missing imports * Communicate login requirements to user on account creation * Add login requirements to basic auth handler * Cleanups and test fix * Encapsulate approval logic in user service and log approval changes * Send follow up "Account approved" email Closes #5656. * Add notification for admins * Fix creating a user via the admin view * Update list: Unify flags into status column, add approve action * Adjust "Resend email" wording * Incorporate feedback from code review * Remove duplicate test server policy reset --------- Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@@ -8,25 +7,21 @@ using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MimeKit;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class UIServerController
|
||||
{
|
||||
[Route("server/users")]
|
||||
[HttpGet("server/users")]
|
||||
public async Task<IActionResult> ListUsers(
|
||||
[FromServices] RoleManager<IdentityRole> roleManager,
|
||||
UsersViewModel model,
|
||||
string sortOrder = null
|
||||
)
|
||||
UsersViewModel model,
|
||||
string sortOrder = null)
|
||||
{
|
||||
model = this.ParseListQuery(model ?? new UsersViewModel());
|
||||
|
||||
@@ -64,7 +59,8 @@ namespace BTCPayServer.Controllers
|
||||
Name = u.UserName,
|
||||
Email = u.Email,
|
||||
Id = u.Id,
|
||||
Verified = u.EmailConfirmed || !u.RequiresEmailConfirmation,
|
||||
EmailConfirmed = u.RequiresEmailConfirmation ? u.EmailConfirmed : null,
|
||||
Approved = u.RequiresApproval ? u.Approved : null,
|
||||
Created = u.Created,
|
||||
Roles = u.UserRoles.Select(role => role.RoleId),
|
||||
Disabled = u.LockoutEnabled && u.LockoutEnd != null && DateTimeOffset.UtcNow < u.LockoutEnd.Value.UtcDateTime
|
||||
@@ -74,44 +70,67 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[Route("server/users/{userId}")]
|
||||
[HttpGet("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 UsersViewModel.UserViewModel
|
||||
var model = new UsersViewModel.UserViewModel
|
||||
{
|
||||
Id = user.Id,
|
||||
Email = user.Email,
|
||||
Verified = user.EmailConfirmed || !user.RequiresEmailConfirmation,
|
||||
EmailConfirmed = user.RequiresEmailConfirmation ? user.EmailConfirmed : null,
|
||||
Approved = user.RequiresApproval ? user.Approved : null,
|
||||
IsAdmin = Roles.HasServerAdmin(roles)
|
||||
};
|
||||
return View(userVM);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[Route("server/users/{userId}")]
|
||||
[HttpPost]
|
||||
[HttpPost("server/users/{userId}")]
|
||||
public new async Task<IActionResult> User(string userId, UsersViewModel.UserViewModel viewModel)
|
||||
{
|
||||
var user = await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
|
||||
bool? propertiesChanged = null;
|
||||
bool? adminStatusChanged = null;
|
||||
bool? approvalStatusChanged = null;
|
||||
|
||||
if (user.RequiresApproval && viewModel.Approved.HasValue)
|
||||
{
|
||||
approvalStatusChanged = await _userService.SetUserApproval(user.Id, viewModel.Approved.Value, Request.GetAbsoluteRootUri());
|
||||
}
|
||||
if (user.RequiresEmailConfirmation && viewModel.EmailConfirmed.HasValue && user.EmailConfirmed != viewModel.EmailConfirmed)
|
||||
{
|
||||
user.EmailConfirmed = viewModel.EmailConfirmed.Value;
|
||||
propertiesChanged = true;
|
||||
}
|
||||
|
||||
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
var wasAdmin = Roles.HasServerAdmin(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
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
if (viewModel.IsAdmin != wasAdmin)
|
||||
{
|
||||
var success = await _userService.SetAdminUser(user.Id, viewModel.IsAdmin);
|
||||
if (success)
|
||||
adminStatusChanged = await _userService.SetAdminUser(user.Id, viewModel.IsAdmin);
|
||||
}
|
||||
|
||||
if (propertiesChanged is true)
|
||||
{
|
||||
propertiesChanged = await _UserManager.UpdateAsync(user) is { Succeeded: true };
|
||||
}
|
||||
|
||||
if (propertiesChanged.HasValue || adminStatusChanged.HasValue || approvalStatusChanged.HasValue)
|
||||
{
|
||||
if (propertiesChanged is not false && adminStatusChanged is not false && approvalStatusChanged is not false)
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] = "User successfully updated";
|
||||
}
|
||||
@@ -121,23 +140,22 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(User), new { userId = userId });
|
||||
return RedirectToAction(nameof(User), new { userId });
|
||||
}
|
||||
|
||||
[Route("server/users/new")]
|
||||
[HttpGet]
|
||||
[HttpGet("server/users/new")]
|
||||
public IActionResult CreateUser()
|
||||
{
|
||||
ViewData["AllowRequestApproval"] = _policiesSettings.RequiresUserApproval;
|
||||
ViewData["AllowRequestEmailConfirmation"] = _policiesSettings.RequiresConfirmedEmail;
|
||||
return View();
|
||||
}
|
||||
|
||||
[Route("server/users/new")]
|
||||
[HttpPost]
|
||||
[HttpPost("server/users/new")]
|
||||
public async Task<IActionResult> CreateUser(RegisterFromAdminViewModel model)
|
||||
{
|
||||
var requiresConfirmedEmail = _policiesSettings.RequiresConfirmedEmail;
|
||||
ViewData["AllowRequestEmailConfirmation"] = requiresConfirmedEmail;
|
||||
ViewData["AllowRequestApproval"] = _policiesSettings.RequiresUserApproval;
|
||||
ViewData["AllowRequestEmailConfirmation"] = _policiesSettings.RequiresConfirmedEmail;
|
||||
if (!_Options.CheatMode)
|
||||
model.IsAdmin = false;
|
||||
if (ModelState.IsValid)
|
||||
@@ -148,7 +166,9 @@ namespace BTCPayServer.Controllers
|
||||
UserName = model.Email,
|
||||
Email = model.Email,
|
||||
EmailConfirmed = model.EmailConfirmed,
|
||||
RequiresEmailConfirmation = requiresConfirmedEmail,
|
||||
RequiresEmailConfirmation = _policiesSettings.RequiresConfirmedEmail,
|
||||
RequiresApproval = _policiesSettings.RequiresUserApproval,
|
||||
Approved = model.Approved,
|
||||
Created = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
@@ -223,7 +243,6 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (await _userService.IsUserTheOnlyOneAdmin(user))
|
||||
{
|
||||
// return
|
||||
return View("Confirm", new ConfirmModel("Delete admin",
|
||||
$"Unable to proceed: As the user <strong>{Html.Encode(user.Email)}</strong> is the last enabled admin, it cannot be removed."));
|
||||
}
|
||||
@@ -281,6 +300,29 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(ListUsers));
|
||||
}
|
||||
|
||||
[HttpGet("server/users/{userId}/approve")]
|
||||
public async Task<IActionResult> ApproveUser(string userId, bool approved)
|
||||
{
|
||||
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
|
||||
return View("Confirm", new ConfirmModel($"{(approved ? "Approve" : "Unapprove")} user", $"The user <strong>{Html.Encode(user.Email)}</strong> will be {(approved ? "approved" : "unapproved")}. Are you sure?", (approved ? "Approve" : "Unapprove")));
|
||||
}
|
||||
|
||||
[HttpPost("server/users/{userId}/approve")]
|
||||
public async Task<IActionResult> ApproveUserPost(string userId, bool approved)
|
||||
{
|
||||
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
|
||||
await _userService.SetUserApproval(userId, approved, Request.GetAbsoluteRootUri());
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"User {(approved ? "approved" : "unapproved")}";
|
||||
return RedirectToAction(nameof(ListUsers));
|
||||
}
|
||||
|
||||
[HttpGet("server/users/{userId}/verification-email")]
|
||||
public async Task<IActionResult> SendVerificationEmail(string userId)
|
||||
{
|
||||
@@ -332,5 +374,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[Display(Name = "Email confirmed?")]
|
||||
public bool EmailConfirmed { get; set; }
|
||||
|
||||
[Display(Name = "User approved?")]
|
||||
public bool Approved { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user