Compare commits
1 Commits
v1.11.1
...
user-disab
Author | SHA1 | Date | |
---|---|---|---|
597116bb26 |
@ -33,6 +33,15 @@ namespace BTCPayServer.Client
|
||||
return await HandleResponse<ApplicationUserData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task ToggleUser(string idOrEmail, bool enabled, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/{idOrEmail}/toggle", null,new
|
||||
{
|
||||
enabled
|
||||
} , HttpMethod.Post), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task<ApplicationUserData[]> GetUsers( CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/", null, HttpMethod.Get), token);
|
||||
|
@ -35,5 +35,7 @@ namespace BTCPayServer.Client.Models
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? Created { get; set; }
|
||||
|
||||
public bool Disabled { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -73,6 +73,19 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
return UserNotFound();
|
||||
}
|
||||
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/users/{idOrEmail}/toggle")]
|
||||
public async Task<IActionResult> ToggleUser(string idOrEmail, bool enabled )
|
||||
{
|
||||
var user = (await _userManager.FindByIdAsync(idOrEmail) ) ?? await _userManager.FindByEmailAsync(idOrEmail);
|
||||
if (user is null)
|
||||
{
|
||||
return UserNotFound();
|
||||
}
|
||||
|
||||
await _userService.ToggleUser(user.Id, enabled ? null : DateTimeOffset.MaxValue);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewUsers, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/users/")]
|
||||
@ -219,7 +232,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
// User shouldn't be deleted if it's the only admin
|
||||
if (await IsUserTheOnlyOneAdmin(user))
|
||||
if (await _userService.IsUserTheOnlyOneAdmin(user))
|
||||
{
|
||||
return Forbid(AuthenticationSchemes.GreenfieldBasic);
|
||||
}
|
||||
@ -236,21 +249,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return UserService.FromModel(data, roles);
|
||||
}
|
||||
|
||||
private async Task<bool> IsUserTheOnlyOneAdmin()
|
||||
{
|
||||
return await IsUserTheOnlyOneAdmin(await _userManager.GetUserAsync(User));
|
||||
}
|
||||
|
||||
private async Task<bool> IsUserTheOnlyOneAdmin(ApplicationUser user)
|
||||
{
|
||||
var isUserAdmin = await _userService.IsAdminUser(user);
|
||||
if (!isUserAdmin)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin)).Count == 1;
|
||||
}
|
||||
|
||||
|
||||
private IActionResult UserNotFound()
|
||||
{
|
||||
|
@ -128,7 +128,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Host = new HostString("dummy.com"),
|
||||
Path = new PathString(),
|
||||
PathBase = new PathString(),
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -189,7 +188,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_storePaymentMethodsController,
|
||||
_greenfieldStoreEmailController,
|
||||
_greenfieldStoreUsersController,
|
||||
new LocalHttpContextAccessor() { HttpContext = context }
|
||||
new LocalHttpContextAccessor() {HttpContext = context}
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -575,7 +574,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
case JsonResult jsonResult:
|
||||
return (T)jsonResult.Value;
|
||||
case OkObjectResult { Value: T res }:
|
||||
case OkObjectResult {Value: T res}:
|
||||
return res;
|
||||
default:
|
||||
return default;
|
||||
@ -586,9 +585,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case UnprocessableEntityObjectResult { Value: List<GreenfieldValidationError> validationErrors }:
|
||||
case UnprocessableEntityObjectResult {Value: List<GreenfieldValidationError> validationErrors}:
|
||||
throw new GreenfieldValidationException(validationErrors.ToArray());
|
||||
case BadRequestObjectResult { Value: GreenfieldAPIError error }:
|
||||
case BadRequestObjectResult {Value: GreenfieldAPIError error}:
|
||||
throw new GreenfieldAPIException(400, error);
|
||||
case NotFoundResult _:
|
||||
throw new GreenfieldAPIException(404, new GreenfieldAPIError("not-found", ""));
|
||||
@ -739,7 +738,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
return GetFromActionResult<NotificationData>(
|
||||
await _notificationsController.UpdateNotification(notificationId,
|
||||
new UpdateNotification() { Seen = seen }));
|
||||
new UpdateNotification() {Seen = seen}));
|
||||
}
|
||||
|
||||
public override async Task RemoveNotification(string notificationId, CancellationToken token = default)
|
||||
@ -1101,5 +1100,26 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
return GetFromActionResult<ApplicationUserData>(await _usersController.GetUser(idOrEmail));
|
||||
}
|
||||
|
||||
public override async Task ToggleUser(string idOrEmail, bool enabled, CancellationToken token = default)
|
||||
{
|
||||
HandleActionResult(await _usersController.ToggleUser(idOrEmail, enabled));
|
||||
}
|
||||
|
||||
public override async Task<OnChainWalletTransactionData> PatchOnChainWalletTransaction(string storeId, string cryptoCode, string transactionId,
|
||||
PatchOnChainTransactionRequest request, CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<OnChainWalletTransactionData>(await _storeOnChainWalletsController.PatchOnChainWalletTransaction(storeId, cryptoCode, transactionId, request));
|
||||
}
|
||||
|
||||
public override async Task<LightningPaymentData> GetLightningPayment(string cryptoCode, string paymentHash, CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<LightningPaymentData>(await _lightningNodeApiController.GetPayment(cryptoCode, paymentHash));
|
||||
}
|
||||
|
||||
public override async Task<LightningPaymentData> GetLightningPayment(string storeId, string cryptoCode, string paymentHash, CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<LightningPaymentData>(await _storeLightningNodeApiController.GetPayment(cryptoCode, paymentHash));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +64,8 @@ namespace BTCPayServer.Controllers
|
||||
Id = u.Id,
|
||||
Verified = u.EmailConfirmed || !u.RequiresEmailConfirmation,
|
||||
Created = u.Created,
|
||||
Roles = u.UserRoles.Select(role => role.RoleId)
|
||||
Roles = u.UserRoles.Select(role => role.RoleId),
|
||||
Disabled = u.LockoutEnabled && u.LockoutEnd != null && DateTimeOffset.UtcNow < u.LockoutEnd.Value.UtcDateTime
|
||||
})
|
||||
.ToListAsync();
|
||||
model.Total = await usersQuery.CountAsync();
|
||||
@ -217,12 +218,11 @@ namespace BTCPayServer.Controllers
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
if (_userService.IsRoleAdmin(roles))
|
||||
{
|
||||
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
if (admins.Count == 1)
|
||||
if (await _userService.IsUserTheOnlyOneAdmin(user))
|
||||
{
|
||||
// return
|
||||
return View("Confirm", new ConfirmModel("Delete admin",
|
||||
$"Unable to proceed: As the user <strong>{user.Email}</strong> is the last admin, it cannot be removed."));
|
||||
$"Unable to proceed: As the user <strong>{user.Email}</strong> is the last enabled admin, it cannot be removed."));
|
||||
}
|
||||
|
||||
return View("Confirm", new ConfirmModel("Delete admin",
|
||||
@ -245,6 +245,41 @@ namespace BTCPayServer.Controllers
|
||||
TempData[WellKnownTempData.SuccessMessage] = "User deleted";
|
||||
return RedirectToAction(nameof(ListUsers));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
[HttpGet("server/users/{userId}/toggle")]
|
||||
public async Task<IActionResult> ToggleUser(string userId, bool enable)
|
||||
{
|
||||
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
|
||||
if (!enable && await _userService.IsUserTheOnlyOneAdmin(user))
|
||||
{
|
||||
return View("Confirm", new ConfirmModel("Disable admin",
|
||||
$"Unable to proceed: As the user <strong>{user.Email}</strong> is the last enabled admin, it cannot be disabled."));
|
||||
}
|
||||
return View("Confirm", new ConfirmModel($"{(enable? "Enable" : "Disable")} user", $"The user <strong>{user.Email}</strong> will be {(enable? "enabled" : "disabled")}. Are you sure?", (enable? "Enable" : "Disable")));
|
||||
}
|
||||
|
||||
[HttpPost("server/users/{userId}/toggle")]
|
||||
public async Task<IActionResult> ToggleUserPost(string userId, bool enable)
|
||||
{
|
||||
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
if (!enable && await _userService.IsUserTheOnlyOneAdmin(user))
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"User was the last enabled admin and could not be disabled.";
|
||||
return RedirectToAction(nameof(ListUsers));
|
||||
}
|
||||
await _userService.ToggleUser(userId, enable? null: DateTimeOffset.MaxValue);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"User {(enable? "enabled": "disabled")}";
|
||||
return RedirectToAction(nameof(ListUsers));
|
||||
}
|
||||
}
|
||||
|
||||
public class RegisterFromAdminViewModel
|
||||
|
@ -11,6 +11,7 @@ namespace BTCPayServer.Models.ServerViewModels
|
||||
public string Name { get; set; }
|
||||
public string Email { get; set; }
|
||||
public bool Verified { get; set; }
|
||||
public bool Disabled { get; set; }
|
||||
public bool IsAdmin { get; set; }
|
||||
public DateTimeOffset? Created { get; set; }
|
||||
public IEnumerable<string> Roles { get; set; }
|
||||
|
@ -16,7 +16,6 @@ namespace BTCPayServer.Services
|
||||
public class UserService
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly StoredFileRepository _storedFileRepository;
|
||||
private readonly FileService _fileService;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
@ -24,7 +23,6 @@ namespace BTCPayServer.Services
|
||||
|
||||
public UserService(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
IAuthorizationService authorizationService,
|
||||
StoredFileRepository storedFileRepository,
|
||||
FileService fileService,
|
||||
StoreRepository storeRepository,
|
||||
@ -33,7 +31,6 @@ namespace BTCPayServer.Services
|
||||
)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_authorizationService = authorizationService;
|
||||
_storedFileRepository = storedFileRepository;
|
||||
_fileService = fileService;
|
||||
_storeRepository = storeRepository;
|
||||
@ -57,10 +54,31 @@ namespace BTCPayServer.Services
|
||||
EmailConfirmed = data.EmailConfirmed,
|
||||
RequiresEmailConfirmation = data.RequiresEmailConfirmation,
|
||||
Created = data.Created,
|
||||
Roles = roles
|
||||
Roles = roles,
|
||||
Disabled = data.LockoutEnabled && data.LockoutEnd is not null && DateTimeOffset.UtcNow < data.LockoutEnd.Value.UtcDateTime
|
||||
};
|
||||
}
|
||||
|
||||
private bool IsDisabled(ApplicationUser user)
|
||||
{
|
||||
return user.LockoutEnabled && user.LockoutEnd is not null &&
|
||||
DateTimeOffset.UtcNow < user.LockoutEnd.Value.UtcDateTime;
|
||||
}
|
||||
public async Task ToggleUser(string userId, DateTimeOffset? lockedOutDeadline)
|
||||
{
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
if (user is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (lockedOutDeadline is not null)
|
||||
{
|
||||
await _userManager.SetLockoutEnabledAsync(user, true);
|
||||
}
|
||||
|
||||
await _userManager.SetLockoutEndDateAsync(user, lockedOutDeadline);
|
||||
}
|
||||
|
||||
public async Task<bool> IsAdminUser(string userId)
|
||||
{
|
||||
return IsRoleAdmin(await _userManager.GetRolesAsync(new ApplicationUser() { Id = userId }));
|
||||
@ -89,5 +107,17 @@ namespace BTCPayServer.Services
|
||||
{
|
||||
return roles.Contains(Roles.ServerAdmin, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> IsUserTheOnlyOneAdmin(ApplicationUser user)
|
||||
{
|
||||
var isUserAdmin = await IsAdminUser(user);
|
||||
if (!isUserAdmin)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin)).Count(applicationUser => !IsDisabled(applicationUser)) == 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,8 +56,9 @@
|
||||
<span class="fa @(sortIconClass)" />
|
||||
</a>
|
||||
</th>
|
||||
<th>Created</th>
|
||||
<th>Verified</th>
|
||||
<th >Created</th>
|
||||
<th class="text-center">Verified</th>
|
||||
<th class="text-center">Disabled</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -83,8 +84,23 @@
|
||||
<span class="text-danger fa fa-times"></span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
@if (user.Disabled)
|
||||
{
|
||||
<span class="text-danger fa fa-check" title="disabled"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-success fa fa-times" title="enabled"></span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a asp-action="User" asp-route-userId="@user.Id">Edit</a> <span> - </span> <a asp-action="DeleteUser" asp-route-userId="@user.Id">Remove</a>
|
||||
- <a asp-action="ToggleUser"
|
||||
asp-route-enable="@user.Disabled"
|
||||
asp-route-userId="@user.Id">
|
||||
@(user.Disabled ? "Enable" : "Disable")
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
Reference in New Issue
Block a user