2020-09-05 12:16:48 +02:00
using System ;
using System.Collections.Generic ;
using System.ComponentModel.DataAnnotations ;
using System.Linq ;
using System.Threading.Tasks ;
2022-02-21 15:46:43 +01:00
using BTCPayServer.Abstractions.Constants ;
2020-11-17 13:46:23 +01:00
using BTCPayServer.Abstractions.Extensions ;
using BTCPayServer.Abstractions.Models ;
2020-09-05 12:16:48 +02:00
using BTCPayServer.Data ;
using BTCPayServer.Events ;
using BTCPayServer.Models ;
using BTCPayServer.Models.ServerViewModels ;
2022-05-26 21:36:47 -07:00
using BTCPayServer.Services ;
2020-09-05 12:16:48 +02:00
using Microsoft.AspNetCore.Identity ;
using Microsoft.AspNetCore.Mvc ;
2022-05-26 21:36:47 -07:00
using Microsoft.AspNetCore.Routing ;
2023-01-06 14:18:07 +01:00
using Microsoft.EntityFrameworkCore ;
2022-06-22 05:05:32 +02:00
using MimeKit ;
2020-09-05 12:16:48 +02:00
namespace BTCPayServer.Controllers
{
2022-01-07 12:32:00 +09:00
public partial class UIServerController
2020-09-05 12:16:48 +02:00
{
[Route("server/users")]
2021-03-29 22:32:44 -07:00
public async Task < IActionResult > ListUsers (
2021-11-15 10:27:19 +01:00
[FromServices] RoleManager < IdentityRole > roleManager ,
UsersViewModel model ,
2021-03-29 22:32:44 -07:00
string sortOrder = null
)
2020-09-05 12:16:48 +02:00
{
2020-10-03 14:12:55 +02:00
model = this . ParseListQuery ( model ? ? new UsersViewModel ( ) ) ;
2021-12-31 16:59:02 +09:00
2021-03-29 22:32:44 -07:00
var usersQuery = _UserManager . Users ;
if ( ! string . IsNullOrWhiteSpace ( model . SearchTerm ) )
{
2021-04-13 17:06:11 +09:00
#pragma warning disable CA1307 // Specify StringComparison
// Entity Framework don't support StringComparison
2021-03-29 22:32:44 -07:00
usersQuery = usersQuery . Where ( u = > u . Email . Contains ( model . SearchTerm ) ) ;
2021-04-13 17:06:11 +09:00
#pragma warning restore CA1307 // Specify StringComparison
2021-03-29 22:32:44 -07:00
}
2021-12-31 16:59:02 +09:00
if ( sortOrder ! = null )
2021-03-29 22:32:44 -07:00
{
switch ( sortOrder )
{
case "desc" :
ViewData [ "NextUserEmailSortOrder" ] = "asc" ;
usersQuery = usersQuery . OrderByDescending ( user = > user . Email ) ;
break ;
case "asc" :
usersQuery = usersQuery . OrderBy ( user = > user . Email ) ;
ViewData [ "NextUserEmailSortOrder" ] = "desc" ;
break ;
}
}
2021-11-15 10:27:19 +01:00
model . Roles = roleManager . Roles . ToDictionary ( role = > role . Id , role = > role . Name ) ;
2021-03-29 22:32:44 -07:00
model . Users = await usersQuery
2021-11-15 10:27:19 +01:00
. Include ( user = > user . UserRoles )
2021-03-29 22:32:44 -07:00
. Skip ( model . Skip )
. Take ( model . Count )
2020-09-05 12:16:48 +02:00
. Select ( u = > new UsersViewModel . UserViewModel
{
Name = u . UserName ,
Email = u . Email ,
2020-10-03 14:12:55 +02:00
Id = u . Id ,
Verified = u . EmailConfirmed | | ! u . RequiresEmailConfirmation ,
2021-11-15 10:27:19 +01:00
Created = u . Created ,
2022-04-26 14:27:35 +02:00
Roles = u . UserRoles . Select ( role = > role . RoleId ) ,
Disabled = u . LockoutEnabled & & u . LockoutEnd ! = null & & DateTimeOffset . UtcNow < u . LockoutEnd . Value . UtcDateTime
2021-03-29 22:32:44 -07:00
} )
. ToListAsync ( ) ;
2021-12-31 16:59:02 +09:00
2020-10-03 14:12:55 +02:00
return View ( model ) ;
2020-09-05 12:16:48 +02:00
}
[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 ) ;
2020-10-03 14:12:55 +02:00
var userVM = new UsersViewModel . UserViewModel
{
Id = user . Id ,
Email = user . Email ,
Verified = user . EmailConfirmed | | ! user . RequiresEmailConfirmation ,
2023-02-15 14:28:34 +09:00
IsAdmin = Roles . HasServerAdmin ( roles )
2020-10-03 14:12:55 +02:00
} ;
2020-09-05 12:16:48 +02:00
return View ( userVM ) ;
}
[Route("server/users/{userId}")]
[HttpPost]
2020-10-03 14:12:55 +02:00
public new async Task < IActionResult > User ( string userId , UsersViewModel . UserViewModel viewModel )
2020-09-05 12:16:48 +02:00
{
var user = await _UserManager . FindByIdAsync ( userId ) ;
if ( user = = null )
return NotFound ( ) ;
var admins = await _UserManager . GetUsersInRoleAsync ( Roles . ServerAdmin ) ;
var roles = await _UserManager . GetRolesAsync ( user ) ;
2023-02-15 14:28:34 +09:00
var wasAdmin = Roles . HasServerAdmin ( roles ) ;
2020-09-05 12:16:48 +02:00
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 )
{
2022-06-06 18:42:59 -07:00
var success = await _userService . SetAdminUser ( user . Id , viewModel . IsAdmin ) ;
if ( success )
{
TempData [ WellKnownTempData . SuccessMessage ] = "User successfully updated" ;
}
2020-09-05 12:16:48 +02:00
else
2022-06-06 18:42:59 -07:00
{
TempData [ WellKnownTempData . ErrorMessage ] = "Error updating user" ;
}
2020-09-05 12:16:48 +02:00
}
return RedirectToAction ( nameof ( User ) , new { userId = userId } ) ;
}
[Route("server/users/new")]
[HttpGet]
2022-05-24 13:18:16 +09:00
public IActionResult CreateUser ( )
2020-09-05 12:16:48 +02:00
{
2022-05-24 13:18:16 +09:00
ViewData [ "AllowRequestEmailConfirmation" ] = _policiesSettings . RequiresConfirmedEmail ;
2020-09-05 12:16:48 +02:00
return View ( ) ;
}
[Route("server/users/new")]
[HttpPost]
public async Task < IActionResult > CreateUser ( RegisterFromAdminViewModel model )
{
2022-05-24 13:18:16 +09:00
var requiresConfirmedEmail = _policiesSettings . RequiresConfirmedEmail ;
2021-07-27 14:08:54 +02:00
ViewData [ "AllowRequestEmailConfirmation" ] = requiresConfirmedEmail ;
2021-10-11 12:32:09 +09:00
if ( ! _Options . CheatMode )
2020-10-08 11:56:58 +09:00
model . IsAdmin = false ;
2020-09-05 12:16:48 +02:00
if ( ModelState . IsValid )
{
IdentityResult result ;
2021-12-31 16:59:02 +09:00
var user = new ApplicationUser
{
UserName = model . Email ,
Email = model . Email ,
EmailConfirmed = model . EmailConfirmed ,
RequiresEmailConfirmation = requiresConfirmedEmail ,
Created = DateTimeOffset . UtcNow
} ;
2020-09-05 12:16:48 +02:00
if ( ! string . IsNullOrEmpty ( model . Password ) )
{
result = await _UserManager . CreateAsync ( user , model . Password ) ;
}
else
{
result = await _UserManager . CreateAsync ( user ) ;
}
2021-12-31 16:59:02 +09:00
2020-09-05 12:16:48 +02:00
if ( result . Succeeded )
{
if ( model . IsAdmin & & ! ( await _UserManager . AddToRoleAsync ( user , Roles . ServerAdmin ) ) . Succeeded )
model . IsAdmin = false ;
var tcs = new TaskCompletionSource < Uri > ( ) ;
2021-12-31 16:59:02 +09:00
2020-09-05 12:16:48 +02:00
_eventAggregator . Publish ( new UserRegisteredEvent ( )
{
2021-12-31 16:59:02 +09:00
RequestUri = Request . GetAbsoluteRootUri ( ) ,
User = user ,
Admin = model . IsAdmin is true ,
CallbackUrlGenerated = tcs
2020-09-05 12:16:48 +02:00
} ) ;
var callbackUrl = await tcs . Task ;
if ( user . RequiresEmailConfirmation & & ! user . EmailConfirmed )
{
2021-12-31 16:59:02 +09:00
2020-09-05 12:16:48 +02:00
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>"
} ) ;
2021-12-31 16:59:02 +09:00
}
else if ( ! await _UserManager . HasPasswordAsync ( user ) )
2020-09-05 12:16:48 +02:00
{
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 ) ) ;
}
2021-12-31 16:59:02 +09:00
2020-09-05 12:16:48 +02:00
foreach ( var error in result . Errors )
{
ModelState . AddModelError ( string . Empty , error . Description ) ;
}
}
// If we got this far, something failed, redisplay form
return View ( model ) ;
}
2021-09-07 04:55:53 +02:00
[HttpGet("server/users/{userId}/delete")]
2020-09-05 12:16:48 +02:00
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 ) ;
2023-02-15 14:28:34 +09:00
if ( Roles . HasServerAdmin ( roles ) )
2020-09-05 12:16:48 +02:00
{
2022-04-26 14:27:35 +02:00
if ( await _userService . IsUserTheOnlyOneAdmin ( user ) )
2020-09-05 12:16:48 +02:00
{
// return
2021-09-07 04:55:53 +02:00
return View ( "Confirm" , new ConfirmModel ( "Delete admin" ,
2023-01-22 03:08:12 +09:00
$"Unable to proceed: As the user <strong>{Html.Encode(user.Email)}</strong> is the last enabled admin, it cannot be removed." ) ) ;
2020-09-05 12:16:48 +02:00
}
2021-09-07 04:55:53 +02:00
return View ( "Confirm" , new ConfirmModel ( "Delete admin" ,
2023-01-22 03:08:12 +09:00
$"The admin <strong>{Html.Encode(user.Email)}</strong> will be permanently deleted. This action will also delete all accounts, users and data associated with the server account. Are you sure?" ,
2020-09-05 12:16:48 +02:00
"Delete" ) ) ;
}
2021-12-31 16:59:02 +09:00
2023-01-22 03:08:12 +09:00
return View ( "Confirm" , new ConfirmModel ( "Delete user" , $"The user <strong>{Html.Encode(user.Email)}</strong> will be permanently deleted. Are you sure?" , "Delete" ) ) ;
2020-09-05 12:16:48 +02:00
}
2021-09-07 04:55:53 +02:00
[HttpPost("server/users/{userId}/delete")]
2020-09-05 12:16:48 +02:00
public async Task < IActionResult > DeleteUserPost ( string userId )
{
var user = userId = = null ? null : await _UserManager . FindByIdAsync ( userId ) ;
if ( user = = null )
return NotFound ( ) ;
2021-03-14 12:24:32 -07:00
await _userService . DeleteUserAndAssociatedData ( user ) ;
2020-09-05 12:16:48 +02:00
TempData [ WellKnownTempData . SuccessMessage ] = "User deleted" ;
return RedirectToAction ( nameof ( ListUsers ) ) ;
}
2022-04-26 14:27:35 +02:00
[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" ,
2023-01-22 03:08:12 +09:00
$"Unable to proceed: As the user <strong>{Html.Encode(user.Email)}</strong> is the last enabled admin, it cannot be disabled." ) ) ;
2022-04-26 14:27:35 +02:00
}
2023-01-22 03:08:12 +09:00
return View ( "Confirm" , new ConfirmModel ( $"{(enable ? " Enable " : " Disable ")} user" , $"The user <strong>{Html.Encode(user.Email)}</strong> will be {(enable ? " enabled " : " disabled ")}. Are you sure?" , ( enable ? "Enable" : "Disable" ) ) ) ;
2022-04-26 14:27:35 +02:00
}
2023-01-06 14:18:07 +01:00
2022-04-26 14:27:35 +02:00
[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 ) ) ;
}
2023-01-06 14:18:07 +01:00
await _userService . ToggleUser ( userId , enable ? null : DateTimeOffset . MaxValue ) ;
2022-04-26 14:27:35 +02:00
2023-01-06 14:18:07 +01:00
TempData [ WellKnownTempData . SuccessMessage ] = $"User {(enable ? " enabled " : " disabled ")}" ;
2022-04-26 14:27:35 +02:00
return RedirectToAction ( nameof ( ListUsers ) ) ;
}
2022-05-26 21:36:47 -07:00
[HttpGet("server/users/{userId}/verification-email")]
public async Task < IActionResult > SendVerificationEmail ( string userId )
{
var user = userId = = null ? null : await _UserManager . FindByIdAsync ( userId ) ;
if ( user = = null )
return NotFound ( ) ;
2023-01-06 14:18:07 +01:00
2023-01-22 03:08:12 +09:00
return View ( "Confirm" , new ConfirmModel ( "Send verification email" , $"This will send a verification email to <strong>{Html.Encode(user.Email)}</strong>." , "Send" ) ) ;
2022-05-26 21:36:47 -07:00
}
[HttpPost("server/users/{userId}/verification-email")]
public async Task < IActionResult > SendVerificationEmailPost ( string userId )
{
var user = await _UserManager . FindByIdAsync ( userId ) ;
if ( user = = null )
{
throw new ApplicationException ( $"Unable to load user with ID '{userId}'." ) ;
}
var code = await _UserManager . GenerateEmailConfirmationTokenAsync ( user ) ;
var callbackUrl = _linkGenerator . EmailConfirmationLink ( user . Id , code , Request . Scheme , Request . Host , Request . PathBase ) ;
2022-06-23 13:41:52 +09:00
( await _emailSenderFactory . GetEmailSender ( ) ) . SendEmailConfirmation ( user . GetMailboxAddress ( ) , callbackUrl ) ;
2022-05-26 21:36:47 -07:00
TempData [ WellKnownTempData . SuccessMessage ] = "Verification email sent" ;
return RedirectToAction ( nameof ( ListUsers ) ) ;
}
2020-09-05 12:16:48 +02:00
}
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?")]
2021-12-31 16:59:02 +09:00
public bool EmailConfirmed { get ; set ; }
2020-09-05 12:16:48 +02:00
}
}