using System; using System.Text.Encodings.Web; using System.Threading.Tasks; using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Fido2; using BTCPayServer.Models; using BTCPayServer.Models.ManageViewModels; using BTCPayServer.Security.Greenfield; using BTCPayServer.Services; using BTCPayServer.Services.Mails; using BTCPayServer.Services.Stores; using BTCPayServer.Services.Wallets; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; using MimeKit; namespace BTCPayServer.Controllers { [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanViewProfile)] [Route("account/{action:lowercase=Index}")] public partial class UIManageController : Controller { private readonly UserManager _userManager; private readonly SignInManager _signInManager; private readonly EmailSenderFactory _EmailSenderFactory; private readonly ILogger _logger; private readonly UrlEncoder _urlEncoder; private readonly BTCPayServerEnvironment _btcPayServerEnvironment; private readonly APIKeyRepository _apiKeyRepository; private readonly IAuthorizationService _authorizationService; private readonly Fido2Service _fido2Service; private readonly LinkGenerator _linkGenerator; private readonly UserLoginCodeService _userLoginCodeService; private readonly IHtmlHelper Html; private readonly UserService _userService; private readonly UriResolver _uriResolver; private readonly IFileService _fileService; readonly StoreRepository _StoreRepository; public UIManageController( UserManager userManager, SignInManager signInManager, EmailSenderFactory emailSenderFactory, ILogger logger, UrlEncoder urlEncoder, StoreRepository storeRepository, BTCPayServerEnvironment btcPayServerEnvironment, APIKeyRepository apiKeyRepository, IAuthorizationService authorizationService, Fido2Service fido2Service, LinkGenerator linkGenerator, UserService userService, UriResolver uriResolver, IFileService fileService, UserLoginCodeService userLoginCodeService, IHtmlHelper htmlHelper ) { _userManager = userManager; _signInManager = signInManager; _EmailSenderFactory = emailSenderFactory; _logger = logger; _urlEncoder = urlEncoder; _btcPayServerEnvironment = btcPayServerEnvironment; _apiKeyRepository = apiKeyRepository; _authorizationService = authorizationService; _fido2Service = fido2Service; _linkGenerator = linkGenerator; _userLoginCodeService = userLoginCodeService; Html = htmlHelper; _userService = userService; _uriResolver = uriResolver; _fileService = fileService; _StoreRepository = storeRepository; } [HttpGet] public async Task Index() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var blob = user.GetBlob() ?? new(); var model = new IndexViewModel { Email = user.Email, Name = blob.Name, ImageUrl = string.IsNullOrEmpty(blob.ImageUrl) ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), UnresolvedUri.Create(blob.ImageUrl)), EmailConfirmed = user.EmailConfirmed, RequiresEmailConfirmation = user.RequiresEmailConfirmation }; return View(model); } [HttpPost] [ValidateAntiForgeryToken] public async Task DisableShowInvoiceStatusChangeHint() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var blob = user.GetBlob() ?? new(); blob.ShowInvoiceStatusChangeHint = false; user.SetBlob(blob); await _userManager.UpdateAsync(user); return RedirectToAction(nameof(Index)); } [HttpPost] [ValidateAntiForgeryToken] public async Task Index(IndexViewModel model, [FromForm] bool RemoveImageFile = false) { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } bool needUpdate = false; var email = user.Email; if (model.Email != email) { if (!(await _userManager.FindByEmailAsync(model.Email) is null)) { TempData[WellKnownTempData.ErrorMessage] = "The email address is already in use with an other account."; return RedirectToAction(nameof(Index)); } var setUserResult = await _userManager.SetUserNameAsync(user, model.Email); if (!setUserResult.Succeeded) { throw new ApplicationException($"Unexpected error occurred setting email for user with ID '{user.Id}'."); } var setEmailResult = await _userManager.SetEmailAsync(user, model.Email); if (!setEmailResult.Succeeded) { throw new ApplicationException($"Unexpected error occurred setting email for user with ID '{user.Id}'."); } needUpdate = true; } var blob = user.GetBlob() ?? new(); if (blob.Name != model.Name) { blob.Name = model.Name; needUpdate = true; } if (model.ImageFile != null) { if (model.ImageFile.Length > 1_000_000) { ModelState.AddModelError(nameof(model.ImageFile), "The uploaded image file should be less than 1MB"); } else if (!model.ImageFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture)) { ModelState.AddModelError(nameof(model.ImageFile), "The uploaded file needs to be an image"); } else { var formFile = await model.ImageFile.Bufferize(); if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName)) { ModelState.AddModelError(nameof(model.ImageFile), "The uploaded file needs to be an image"); } else { model.ImageFile = formFile; // add new image try { var storedFile = await _fileService.AddFile(model.ImageFile, user.Id); var fileIdUri = new UnresolvedUri.FileIdUri(storedFile.Id); blob.ImageUrl = fileIdUri.ToString(); needUpdate = true; } catch (Exception e) { ModelState.AddModelError(nameof(model.ImageFile), $"Could not save image: {e.Message}"); } } } } else if (RemoveImageFile && !string.IsNullOrEmpty(blob.ImageUrl)) { blob.ImageUrl = null; needUpdate = true; } user.SetBlob(blob); if (!ModelState.IsValid) { return View(model); } if (needUpdate is true) { needUpdate = await _userManager.UpdateAsync(user) is { Succeeded: true }; TempData[WellKnownTempData.SuccessMessage] = "Your profile has been updated"; } else { TempData[WellKnownTempData.ErrorMessage] = "Error updating profile"; } return RedirectToAction(nameof(Index)); } [HttpPost] [ValidateAntiForgeryToken] public async Task SendVerificationEmail(IndexViewModel model) { if (!ModelState.IsValid) { return View(nameof(Index), model); } var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); var callbackUrl = _linkGenerator.EmailConfirmationLink(user.Id, code, Request.Scheme, Request.Host, Request.PathBase); (await _EmailSenderFactory.GetEmailSender()).SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl); TempData[WellKnownTempData.SuccessMessage] = "Verification email sent. Please check your email."; return RedirectToAction(nameof(Index)); } [HttpGet] public async Task ChangePassword() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var hasPassword = await _userManager.HasPasswordAsync(user); if (!hasPassword) { return RedirectToAction(nameof(SetPassword)); } var model = new ChangePasswordViewModel(); return View(model); } [HttpPost] [ValidateAntiForgeryToken] public async Task ChangePassword(ChangePasswordViewModel model) { if (!ModelState.IsValid) { return View(model); } var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var changePasswordResult = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword); if (!changePasswordResult.Succeeded) { AddErrors(changePasswordResult); return View(model); } await _signInManager.SignInAsync(user, isPersistent: false); _logger.LogInformation("User changed their password successfully."); TempData[WellKnownTempData.SuccessMessage] = "Your password has been changed."; return RedirectToAction(nameof(ChangePassword)); } [HttpGet] public async Task SetPassword() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var hasPassword = await _userManager.HasPasswordAsync(user); if (hasPassword) { return RedirectToAction(nameof(ChangePassword)); } var model = new SetPasswordViewModel(); return View(model); } [HttpPost] [ValidateAntiForgeryToken] public async Task SetPassword(SetPasswordViewModel model) { if (!ModelState.IsValid) { return View(model); } var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var addPasswordResult = await _userManager.AddPasswordAsync(user, model.NewPassword); if (!addPasswordResult.Succeeded) { AddErrors(addPasswordResult); return View(model); } await _signInManager.SignInAsync(user, isPersistent: false); TempData[WellKnownTempData.SuccessMessage] = "Your password has been set."; return RedirectToAction(nameof(SetPassword)); } [HttpPost()] public async Task DeleteUserPost() { var user = await _userManager.GetUserAsync(User); if (user == null) { return NotFound(); } await _userService.DeleteUserAndAssociatedData(user); TempData[WellKnownTempData.SuccessMessage] = "Account successfully deleted."; await _signInManager.SignOutAsync(); return RedirectToAction(nameof(UIAccountController.Login), "UIAccount"); } #region Helpers private void AddErrors(IdentityResult result) { foreach (var error in result.Errors) { ModelState.AddModelError(string.Empty, error.Description); } } #endregion } }