2020-09-05 12:16:48 +02:00
using System ;
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.ServerViewModels ;
2022-05-26 21:36:47 -07:00
using BTCPayServer.Services ;
2024-02-28 12:43:18 +01:00
using BTCPayServer.Services.Mails ;
2020-09-05 12:16:48 +02:00
using Microsoft.AspNetCore.Identity ;
using Microsoft.AspNetCore.Mvc ;
2023-01-06 14:18:07 +01:00
using Microsoft.EntityFrameworkCore ;
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
{
2024-01-31 06:45:54 +01:00
[HttpGet("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 ,
2024-01-31 06:45:54 +01:00
UsersViewModel model ,
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 ) ;
2024-06-26 10:39:22 +02:00
model . Users = ( await usersQuery
2021-11-15 10:27:19 +01:00
. Include ( user = > user . UserRoles )
2024-02-23 09:51:41 +01:00
. Include ( user = > user . UserStores )
. ThenInclude ( data = > data . StoreData )
2021-03-29 22:32:44 -07:00
. Skip ( model . Skip )
. Take ( model . Count )
2024-06-26 10:39:22 +02:00
. ToListAsync ( ) )
2020-09-05 12:16:48 +02:00
. Select ( u = > new UsersViewModel . UserViewModel
{
2024-06-26 10:39:22 +02:00
Name = u . GetBlob ( ) ? . Name ,
ImageUrl = u . GetBlob ( ) ? . ImageUrl ,
2020-09-05 12:16:48 +02:00
Email = u . Email ,
2020-10-03 14:12:55 +02:00
Id = u . Id ,
2024-01-31 06:45:54 +01:00
EmailConfirmed = u . RequiresEmailConfirmation ? u . EmailConfirmed : null ,
Approved = u . RequiresApproval ? u . Approved : null ,
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 ) ,
2024-02-23 09:51:41 +01:00
Disabled = u . LockoutEnabled & & u . LockoutEnd ! = null & & DateTimeOffset . UtcNow < u . LockoutEnd . Value . UtcDateTime ,
Stores = u . UserStores . OrderBy ( s = > ! s . StoreData . Archived ) . ToList ( )
2021-03-29 22:32:44 -07:00
} )
2024-06-26 10:39:22 +02:00
. ToList ( ) ;
2020-10-03 14:12:55 +02:00
return View ( model ) ;
2020-09-05 12:16:48 +02:00
}
2024-01-31 06:45:54 +01:00
[HttpGet("server/users/{userId}")]
2020-09-05 12:16:48 +02:00
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 ) ;
2024-06-26 10:39:22 +02:00
var blob = user . GetBlob ( ) ;
2024-01-31 06:45:54 +01:00
var model = new UsersViewModel . UserViewModel
2020-10-03 14:12:55 +02:00
{
Id = user . Id ,
Email = user . Email ,
2024-06-26 10:39:22 +02:00
Name = blob ? . Name ,
ImageUrl = string . IsNullOrEmpty ( blob ? . ImageUrl ) ? null : await _uriResolver . Resolve ( Request . GetAbsoluteRootUri ( ) , UnresolvedUri . Create ( blob . ImageUrl ) ) ,
2024-01-31 06:45:54 +01:00
EmailConfirmed = user . RequiresEmailConfirmation ? user . EmailConfirmed : null ,
Approved = user . RequiresApproval ? user . Approved : null ,
2023-02-15 14:28:34 +09:00
IsAdmin = Roles . HasServerAdmin ( roles )
2020-10-03 14:12:55 +02:00
} ;
2024-01-31 06:45:54 +01:00
return View ( model ) ;
2020-09-05 12:16:48 +02:00
}
2024-01-31 06:45:54 +01:00
[HttpPost("server/users/{userId}")]
2024-06-26 10:39:22 +02:00
public new async Task < IActionResult > User ( string userId , UsersViewModel . UserViewModel viewModel , [ FromForm ] bool RemoveImageFile = false )
2020-09-05 12:16:48 +02:00
{
var user = await _UserManager . FindByIdAsync ( userId ) ;
if ( user = = null )
return NotFound ( ) ;
2024-01-31 06:45:54 +01:00
bool? propertiesChanged = null ;
bool? adminStatusChanged = null ;
bool? approvalStatusChanged = null ;
2024-02-28 12:43:18 +01:00
if ( user . RequiresApproval & & viewModel . Approved . HasValue & & user . Approved ! = viewModel . Approved . Value )
2024-01-31 06:45:54 +01:00
{
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 ;
}
2024-06-26 10:39:22 +02:00
var blob = user . GetBlob ( ) ? ? new ( ) ;
if ( blob . Name ! = viewModel . Name )
{
blob . Name = viewModel . Name ;
propertiesChanged = true ;
}
if ( viewModel . ImageFile ! = null )
{
if ( viewModel . ImageFile . Length > 1_000_000 )
{
ModelState . AddModelError ( nameof ( viewModel . ImageFile ) , "The uploaded image file should be less than 1MB" ) ;
}
else if ( ! viewModel . ImageFile . ContentType . StartsWith ( "image/" , StringComparison . InvariantCulture ) )
{
ModelState . AddModelError ( nameof ( viewModel . ImageFile ) , "The uploaded file needs to be an image" ) ;
}
else
{
var formFile = await viewModel . ImageFile . Bufferize ( ) ;
if ( ! FileTypeDetector . IsPicture ( formFile . Buffer , formFile . FileName ) )
{
ModelState . AddModelError ( nameof ( viewModel . ImageFile ) , "The uploaded file needs to be an image" ) ;
}
else
{
viewModel . ImageFile = formFile ;
// add new image
try
{
var storedFile = await _fileService . AddFile ( viewModel . ImageFile , userId ) ;
var fileIdUri = new UnresolvedUri . FileIdUri ( storedFile . Id ) ;
blob . ImageUrl = fileIdUri . ToString ( ) ;
propertiesChanged = true ;
}
catch ( Exception e )
{
ModelState . AddModelError ( nameof ( viewModel . ImageFile ) , $"Could not save image: {e.Message}" ) ;
}
}
}
}
else if ( RemoveImageFile & & ! string . IsNullOrEmpty ( blob . ImageUrl ) )
{
blob . ImageUrl = null ;
propertiesChanged = true ;
}
user . SetBlob ( blob ) ;
2020-09-05 12:16:48 +02:00
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." ;
2024-01-31 06:45:54 +01:00
return View ( viewModel ) ;
2020-09-05 12:16:48 +02:00
}
if ( viewModel . IsAdmin ! = wasAdmin )
{
2024-01-31 06:45:54 +01:00
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 )
2022-06-06 18:42:59 -07:00
{
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
}
2024-01-31 06:45:54 +01:00
return RedirectToAction ( nameof ( User ) , new { userId } ) ;
2020-09-05 12:16:48 +02:00
}
2024-01-31 06:45:54 +01:00
[HttpGet("server/users/new")]
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 ( ) ;
}
2024-01-31 06:45:54 +01:00
[HttpPost("server/users/new")]
2020-09-05 12:16:48 +02:00
public async Task < IActionResult > CreateUser ( RegisterFromAdminViewModel model )
{
2024-01-31 06:45:54 +01:00
ViewData [ "AllowRequestEmailConfirmation" ] = _policiesSettings . 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 )
{
2021-12-31 16:59:02 +09:00
var user = new ApplicationUser
{
UserName = model . Email ,
Email = model . Email ,
EmailConfirmed = model . EmailConfirmed ,
2024-01-31 06:45:54 +01:00
RequiresEmailConfirmation = _policiesSettings . RequiresConfirmedEmail ,
RequiresApproval = _policiesSettings . RequiresUserApproval ,
2024-02-28 12:43:18 +01:00
Approved = true , // auto-approve users created by an admin
2021-12-31 16:59:02 +09:00
Created = DateTimeOffset . UtcNow
} ;
2020-09-05 12:16:48 +02:00
2024-02-28 12:43:18 +01:00
var result = string . IsNullOrEmpty ( model . Password )
? await _UserManager . CreateAsync ( user )
: await _UserManager . CreateAsync ( user , model . Password ) ;
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 > ( ) ;
2024-02-28 12:43:18 +01:00
var currentUser = await _UserManager . GetUserAsync ( HttpContext . User ) ;
2021-12-31 16:59:02 +09:00
2024-02-28 12:43:18 +01:00
_eventAggregator . Publish ( new UserRegisteredEvent
2020-09-05 12:16:48 +02:00
{
2021-12-31 16:59:02 +09:00
RequestUri = Request . GetAbsoluteRootUri ( ) ,
2024-03-19 14:58:33 +01:00
Kind = UserRegisteredEventKind . Invite ,
2021-12-31 16:59:02 +09:00
User = user ,
2024-02-28 12:43:18 +01:00
InvitedByUser = currentUser ,
Admin = model . IsAdmin ,
2021-12-31 16:59:02 +09:00
CallbackUrlGenerated = tcs
2020-09-05 12:16:48 +02:00
} ) ;
2024-02-28 12:43:18 +01:00
2020-09-05 12:16:48 +02:00
var callbackUrl = await tcs . Task ;
2024-02-28 12:43:18 +01:00
var settings = await _SettingsRepository . GetSettingAsync < EmailSettings > ( ) ? ? new EmailSettings ( ) ;
var info = settings . IsComplete ( )
? "An invitation email has been sent.<br/>You may alternatively"
: "An invitation email has not been sent, because the server does not have an email server configured.<br/> You need to" ;
TempData . SetStatusMessageModel ( new StatusMessageModel
2020-09-05 12:16:48 +02:00
{
2024-02-28 12:43:18 +01:00
Severity = StatusMessageModel . StatusSeverity . Success ,
AllowDismiss = false ,
Html = $"Account successfully created. {info} share this link with them: <a class='alert-link' href='{callbackUrl}'>{callbackUrl}</a>"
} ) ;
2020-09-05 12:16:48 +02:00
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
{
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
2024-01-31 06:45:54 +01:00
[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 ) ) ;
}
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
}
}