From 71ba5d9c4c2d095794fd33f05c09c3891369d9dc Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Thu, 4 Apr 2024 10:47:28 +0200 Subject: [PATCH 1/4] Move actions and methods to separate partial controllers --- .../UIStoresController.Dashboard.cs | 58 ++ .../UIStoresController.Integrations.cs | 1 - .../UIStoresController.LightningLike.cs | 5 - .../Controllers/UIStoresController.Onchain.cs | 26 +- .../Controllers/UIStoresController.Rates.cs | 191 ++++ .../UIStoresController.Settings.cs | 447 +++++++++ .../Controllers/UIStoresController.Tokens.cs | 270 +++++ .../Controllers/UIStoresController.Users.cs | 24 - .../Controllers/UIStoresController.cs | 939 +----------------- 9 files changed, 991 insertions(+), 970 deletions(-) create mode 100644 BTCPayServer/Controllers/UIStoresController.Rates.cs create mode 100644 BTCPayServer/Controllers/UIStoresController.Settings.cs create mode 100644 BTCPayServer/Controllers/UIStoresController.Tokens.cs diff --git a/BTCPayServer/Controllers/UIStoresController.Dashboard.cs b/BTCPayServer/Controllers/UIStoresController.Dashboard.cs index b169ac61e..91ff69b7f 100644 --- a/BTCPayServer/Controllers/UIStoresController.Dashboard.cs +++ b/BTCPayServer/Controllers/UIStoresController.Dashboard.cs @@ -1,4 +1,5 @@ #nullable enable +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Abstractions.Constants; @@ -9,8 +10,11 @@ using BTCPayServer.Components.StoreRecentInvoices; using BTCPayServer.Components.StoreRecentTransactions; using BTCPayServer.Data; using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Payments.Bitcoin; +using BTCPayServer.Payments.Lightning; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using NBitcoin; namespace BTCPayServer.Controllers { @@ -109,5 +113,59 @@ namespace BTCPayServer.Controllers var vm = new StoreRecentInvoicesViewModel { Store = store, CryptoCode = cryptoCode }; return ViewComponent("StoreRecentInvoices", new { vm }); } + + internal void AddPaymentMethods(StoreData store, StoreBlob storeBlob, + out List derivationSchemes, out List lightningNodes) + { + var excludeFilters = storeBlob.GetExcludedPaymentMethods(); + var derivationByCryptoCode = + store + .GetPaymentMethodConfigs(_handlers) + .ToDictionary(c => ((IHasNetwork)_handlers[c.Key]).Network.CryptoCode, c => (DerivationSchemeSettings)c.Value); + + var lightningByCryptoCode = store + .GetPaymentMethodConfigs(_handlers) + .Where(c => c.Value is LightningPaymentMethodConfig) + .ToDictionary(c => ((IHasNetwork)_handlers[c.Key]).Network.CryptoCode, c => (LightningPaymentMethodConfig)c.Value); + + derivationSchemes = new List(); + lightningNodes = new List(); + + foreach (var handler in _handlers) + { + if (handler is BitcoinLikePaymentHandler { Network: var network }) + { + var strategy = derivationByCryptoCode.TryGet(network.CryptoCode); + var value = strategy?.ToPrettyString() ?? string.Empty; + + derivationSchemes.Add(new StoreDerivationScheme + { + Crypto = network.CryptoCode, + PaymentMethodId = handler.PaymentMethodId, + WalletSupported = network.WalletSupported, + Value = value, + WalletId = new WalletId(store.Id, network.CryptoCode), + Enabled = !excludeFilters.Match(handler.PaymentMethodId) && strategy != null, +#if ALTCOINS + Collapsed = network is Plugins.Altcoins.ElementsBTCPayNetwork elementsBTCPayNetwork && elementsBTCPayNetwork.NetworkCryptoCode != elementsBTCPayNetwork.CryptoCode && string.IsNullOrEmpty(value) +#endif + }); + } + else if (handler is LightningLikePaymentHandler) + { + var lnNetwork = ((IHasNetwork)handler).Network; + var lightning = lightningByCryptoCode.TryGet(lnNetwork.CryptoCode); + var isEnabled = !excludeFilters.Match(handler.PaymentMethodId) && lightning != null; + lightningNodes.Add(new StoreLightningNode + { + CryptoCode = lnNetwork.CryptoCode, + PaymentMethodId = handler.PaymentMethodId, + Address = lightning?.GetDisplayableConnectionString(), + Enabled = isEnabled + }); + } + } + } + } } diff --git a/BTCPayServer/Controllers/UIStoresController.Integrations.cs b/BTCPayServer/Controllers/UIStoresController.Integrations.cs index 573dd8d08..d93e4030a 100644 --- a/BTCPayServer/Controllers/UIStoresController.Integrations.cs +++ b/BTCPayServer/Controllers/UIStoresController.Integrations.cs @@ -7,7 +7,6 @@ using BTCPayServer.Abstractions.Models; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Data; -using BTCPayServer.Models; using BTCPayServer.Models.StoreViewModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/BTCPayServer/Controllers/UIStoresController.LightningLike.cs b/BTCPayServer/Controllers/UIStoresController.LightningLike.cs index 0adaef8fd..3fcf86e41 100644 --- a/BTCPayServer/Controllers/UIStoresController.LightningLike.cs +++ b/BTCPayServer/Controllers/UIStoresController.LightningLike.cs @@ -8,17 +8,12 @@ using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Client; using BTCPayServer.Configuration; using BTCPayServer.Data; -using BTCPayServer.Lightning; -using BTCPayServer.Logging; using BTCPayServer.Models; using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; -using BTCPayServer.Services; -using BTCPayServer.Services.Invoices; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json.Linq; namespace BTCPayServer.Controllers diff --git a/BTCPayServer/Controllers/UIStoresController.Onchain.cs b/BTCPayServer/Controllers/UIStoresController.Onchain.cs index 5669b5eed..eda13c622 100644 --- a/BTCPayServer/Controllers/UIStoresController.Onchain.cs +++ b/BTCPayServer/Controllers/UIStoresController.Onchain.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Abstractions.Constants; @@ -13,9 +14,7 @@ using BTCPayServer.Events; using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; -using BTCPayServer.Services; using Microsoft.AspNetCore.Authorization; -using BTCPayServer.Services.Invoices; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using NBitcoin; @@ -838,5 +837,28 @@ namespace BTCPayServer.Controllers return WalletWarning(isHotWallet, $"The store won't be able to receive {cryptoCode} onchain payments until a new wallet is set up."); } + + private DerivationSchemeSettings ParseDerivationStrategy(string derivationScheme, BTCPayNetwork network) + { + var parser = new DerivationSchemeParser(network); + var isOD = Regex.Match(derivationScheme, @"\(.*?\)"); + if (isOD.Success) + { + var derivationSchemeSettings = new DerivationSchemeSettings(); + var result = parser.ParseOutputDescriptor(derivationScheme); + derivationSchemeSettings.AccountOriginal = derivationScheme.Trim(); + derivationSchemeSettings.AccountDerivation = result.Item1; + derivationSchemeSettings.AccountKeySettings = result.Item2?.Select((path, i) => new AccountKeySettings() + { + RootFingerprint = path?.MasterFingerprint, + AccountKeyPath = path?.KeyPath, + AccountKey = result.Item1.GetExtPubKeys().ElementAt(i).GetWif(parser.Network) + }).ToArray() ?? new AccountKeySettings[result.Item1.GetExtPubKeys().Count()]; + return derivationSchemeSettings; + } + + var strategy = parser.Parse(derivationScheme); + return new DerivationSchemeSettings(strategy, network); + } } } diff --git a/BTCPayServer/Controllers/UIStoresController.Rates.cs b/BTCPayServer/Controllers/UIStoresController.Rates.cs new file mode 100644 index 000000000..d88cc18a1 --- /dev/null +++ b/BTCPayServer/Controllers/UIStoresController.Rates.cs @@ -0,0 +1,191 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Client; +using BTCPayServer.Data; +using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Rating; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Controllers +{ + public partial class UIStoresController + { + [HttpGet("{storeId}/rates")] + public IActionResult Rates() + { + var exchanges = GetSupportedExchanges(); + var storeBlob = CurrentStore.GetStoreBlob(); + var vm = new RatesViewModel(); + vm.SetExchangeRates(exchanges, storeBlob.PreferredExchange ?? storeBlob.GetRecommendedExchange()); + vm.Spread = (double)(storeBlob.Spread * 100m); + vm.StoreId = CurrentStore.Id; + vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString(); + vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString(); + vm.AvailableExchanges = exchanges; + vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString(); + vm.ShowScripting = storeBlob.RateScripting; + return View(vm); + } + + [HttpPost("{storeId}/rates")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task Rates(RatesViewModel model, string? command = null, string? storeId = null, CancellationToken cancellationToken = default) + { + if (command == "scripting-on") + { + return RedirectToAction(nameof(ShowRateRules), new { scripting = true, storeId = model.StoreId }); + } + else if (command == "scripting-off") + { + return RedirectToAction(nameof(ShowRateRules), new { scripting = false, storeId = model.StoreId }); + } + + var exchanges = GetSupportedExchanges(); + model.SetExchangeRates(exchanges, model.PreferredExchange ?? this.HttpContext.GetStoreData().GetStoreBlob().GetRecommendedExchange()); + model.StoreId = storeId ?? model.StoreId; + CurrencyPair[]? currencyPairs = null; + try + { + currencyPairs = model.DefaultCurrencyPairs? + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(p => CurrencyPair.Parse(p)) + .ToArray(); + } + catch + { + ModelState.AddModelError(nameof(model.DefaultCurrencyPairs), "Invalid currency pairs (should be for example: BTC_USD,BTC_CAD,BTC_JPY)"); + } + if (!ModelState.IsValid) + { + return View(model); + } + if (model.PreferredExchange != null) + model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant(); + + var blob = CurrentStore.GetStoreBlob(); + model.DefaultScript = blob.GetDefaultRateRules(_NetworkProvider).ToString(); + model.AvailableExchanges = exchanges; + + blob.PreferredExchange = model.PreferredExchange; + blob.Spread = (decimal)model.Spread / 100.0m; + blob.DefaultCurrencyPairs = currencyPairs; + if (!model.ShowScripting) + { + if (!exchanges.Any(provider => provider.Id.Equals(model.PreferredExchange, StringComparison.InvariantCultureIgnoreCase))) + { + ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})"); + return View(model); + } + } + RateRules? rules = null; + if (model.ShowScripting) + { + if (!RateRules.TryParse(model.Script, out rules, out var errors)) + { + errors = errors ?? new List(); + var errorString = String.Join(", ", errors.ToArray()); + ModelState.AddModelError(nameof(model.Script), $"Parsing error ({errorString})"); + return View(model); + } + else + { + blob.RateScript = rules.ToString(); + ModelState.Remove(nameof(model.Script)); + model.Script = blob.RateScript; + } + } + rules = blob.GetRateRules(_NetworkProvider); + + if (command == "Test") + { + if (string.IsNullOrWhiteSpace(model.ScriptTest)) + { + ModelState.AddModelError(nameof(model.ScriptTest), "Fill out currency pair to test for (like BTC_USD,BTC_CAD)"); + return View(model); + } + var splitted = model.ScriptTest.Split(',', StringSplitOptions.RemoveEmptyEntries); + + var pairs = new List(); + foreach (var pair in splitted) + { + if (!CurrencyPair.TryParse(pair, out var currencyPair)) + { + ModelState.AddModelError(nameof(model.ScriptTest), $"Invalid currency pair '{pair}' (it should be formatted like BTC_USD,BTC_CAD)"); + return View(model); + } + pairs.Add(currencyPair); + } + + var fetchs = _RateFactory.FetchRates(pairs.ToHashSet(), rules, cancellationToken); + var testResults = new List(); + foreach (var fetch in fetchs) + { + var testResult = await (fetch.Value); + testResults.Add(new RatesViewModel.TestResultViewModel() + { + CurrencyPair = fetch.Key.ToString(), + Error = testResult.Errors.Count != 0, + Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.BidAsk.Bid.ToString(CultureInfo.InvariantCulture) + : testResult.EvaluatedRule + }); + } + model.TestRateRules = testResults; + return View(model); + } + else // command == Save + { + if (CurrentStore.SetStoreBlob(blob)) + { + await _Repo.UpdateStore(CurrentStore); + TempData[WellKnownTempData.SuccessMessage] = "Rate settings updated"; + } + return RedirectToAction(nameof(Rates), new + { + storeId = CurrentStore.Id + }); + } + } + + [HttpGet("{storeId}/rates/confirm")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public IActionResult ShowRateRules(bool scripting) + { + return View("Confirm", new ConfirmModel + { + Action = "Continue", + Title = "Rate rule scripting", + Description = scripting ? + "This action will modify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)" + : "This action will delete your rate script. Are you sure to turn off rate rules scripting?", + ButtonClass = scripting ? "btn-primary" : "btn-danger" + }); + } + + [HttpPost("{storeId}/rates/confirm")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task ShowRateRulesPost(bool scripting) + { + var blob = CurrentStore.GetStoreBlob(); + blob.RateScripting = scripting; + blob.RateScript = blob.GetDefaultRateRules(_NetworkProvider).ToString(); + CurrentStore.SetStoreBlob(blob); + await _Repo.UpdateStore(CurrentStore); + TempData[WellKnownTempData.SuccessMessage] = "Rate rules scripting " + (scripting ? "activated" : "deactivated"); + return RedirectToAction(nameof(Rates), new { storeId = CurrentStore.Id }); + } + + private IEnumerable GetSupportedExchanges() + { + return _RateFactory.RateProviderFactory.AvailableRateProviders + .OrderBy(s => s.DisplayName, StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/BTCPayServer/Controllers/UIStoresController.Settings.cs b/BTCPayServer/Controllers/UIStoresController.Settings.cs new file mode 100644 index 000000000..db3013557 --- /dev/null +++ b/BTCPayServer/Controllers/UIStoresController.Settings.cs @@ -0,0 +1,447 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Client; +using BTCPayServer.Data; +using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Payments; +using BTCPayServer.Payments.Lightning; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace BTCPayServer.Controllers +{ + public partial class UIStoresController + { + [HttpGet("{storeId}/settings")] + public IActionResult GeneralSettings() + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + + var storeBlob = store.GetStoreBlob(); + var vm = new GeneralSettingsViewModel + { + Id = store.Id, + StoreName = store.StoreName, + StoreWebsite = store.StoreWebsite, + LogoFileId = storeBlob.LogoFileId, + CssFileId = storeBlob.CssFileId, + BrandColor = storeBlob.BrandColor, + NetworkFeeMode = storeBlob.NetworkFeeMode, + AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice, + PaymentTolerance = storeBlob.PaymentTolerance, + InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes, + DefaultCurrency = storeBlob.DefaultCurrency, + BOLT11Expiration = (long)storeBlob.RefundBOLT11Expiration.TotalDays, + Archived = store.Archived, + CanDelete = _Repo.CanDeleteStores() + }; + + return View(vm); + } + + [HttpPost("{storeId}/settings")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task GeneralSettings( + GeneralSettingsViewModel model, + [FromForm] bool RemoveLogoFile = false, + [FromForm] bool RemoveCssFile = false) + { + bool needUpdate = false; + if (CurrentStore.StoreName != model.StoreName) + { + needUpdate = true; + CurrentStore.StoreName = model.StoreName; + } + + if (CurrentStore.StoreWebsite != model.StoreWebsite) + { + needUpdate = true; + CurrentStore.StoreWebsite = model.StoreWebsite; + } + + var blob = CurrentStore.GetStoreBlob(); + blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice; + blob.NetworkFeeMode = model.NetworkFeeMode; + blob.PaymentTolerance = model.PaymentTolerance; + blob.DefaultCurrency = model.DefaultCurrency; + blob.InvoiceExpiration = TimeSpan.FromMinutes(model.InvoiceExpiration); + blob.RefundBOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration); + if (!string.IsNullOrEmpty(model.BrandColor) && !ColorPalette.IsValid(model.BrandColor)) + { + ModelState.AddModelError(nameof(model.BrandColor), "Invalid color"); + return View(model); + } + blob.BrandColor = model.BrandColor; + + var userId = GetUserId(); + if (userId is null) + return NotFound(); + + if (model.LogoFile != null) + { + if (model.LogoFile.Length > 1_000_000) + { + ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file should be less than 1MB"); + } + else if (!model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture)) + { + ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file needs to be an image"); + } + else + { + var formFile = await model.LogoFile.Bufferize(); + if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName)) + { + ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file needs to be an image"); + } + else + { + model.LogoFile = formFile; + // delete existing file + if (!string.IsNullOrEmpty(blob.LogoFileId)) + { + await _fileService.RemoveFile(blob.LogoFileId, userId); + } + // add new image + try + { + var storedFile = await _fileService.AddFile(model.LogoFile, userId); + blob.LogoFileId = storedFile.Id; + } + catch (Exception e) + { + ModelState.AddModelError(nameof(model.LogoFile), $"Could not save logo: {e.Message}"); + } + } + } + } + else if (RemoveLogoFile && !string.IsNullOrEmpty(blob.LogoFileId)) + { + await _fileService.RemoveFile(blob.LogoFileId, userId); + blob.LogoFileId = null; + needUpdate = true; + } + + if (model.CssFile != null) + { + if (model.CssFile.Length > 1_000_000) + { + ModelState.AddModelError(nameof(model.CssFile), "The uploaded file should be less than 1MB"); + } + else if (!model.CssFile.ContentType.Equals("text/css", StringComparison.InvariantCulture)) + { + ModelState.AddModelError(nameof(model.CssFile), "The uploaded file needs to be a CSS file"); + } + else if (!model.CssFile.FileName.EndsWith(".css", StringComparison.OrdinalIgnoreCase)) + { + ModelState.AddModelError(nameof(model.CssFile), "The uploaded file needs to be a CSS file"); + } + else + { + // delete existing file + if (!string.IsNullOrEmpty(blob.CssFileId)) + { + await _fileService.RemoveFile(blob.CssFileId, userId); + } + // add new file + try + { + var storedFile = await _fileService.AddFile(model.CssFile, userId); + blob.CssFileId = storedFile.Id; + } + catch (Exception e) + { + ModelState.AddModelError(nameof(model.CssFile), $"Could not save CSS file: {e.Message}"); + } + } + } + else if (RemoveCssFile && !string.IsNullOrEmpty(blob.CssFileId)) + { + await _fileService.RemoveFile(blob.CssFileId, userId); + blob.CssFileId = null; + needUpdate = true; + } + + if (CurrentStore.SetStoreBlob(blob)) + { + needUpdate = true; + } + + if (needUpdate) + { + await _Repo.UpdateStore(CurrentStore); + + TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated"; + } + + return RedirectToAction(nameof(GeneralSettings), new + { + storeId = CurrentStore.Id + }); + } + + [HttpPost("{storeId}/archive")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task ToggleArchive(string storeId) + { + CurrentStore.Archived = !CurrentStore.Archived; + await _Repo.UpdateStore(CurrentStore); + + TempData[WellKnownTempData.SuccessMessage] = CurrentStore.Archived + ? "The store has been archived and will no longer appear in the stores list by default." + : "The store has been unarchived and will appear in the stores list by default again."; + + return RedirectToAction(nameof(GeneralSettings), new + { + storeId = CurrentStore.Id + }); + } + + [HttpGet("{storeId}/delete")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public IActionResult DeleteStore(string storeId) + { + return View("Confirm", new ConfirmModel("Delete store", "The store will be permanently deleted. This action will also delete all invoices, apps and data associated with the store. Are you sure?", "Delete")); + } + + [HttpPost("{storeId}/delete")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task DeleteStorePost(string storeId) + { + await _Repo.DeleteStore(CurrentStore.Id); + TempData[WellKnownTempData.SuccessMessage] = "Store successfully deleted."; + return RedirectToAction(nameof(UIHomeController.Index), "UIHome"); + } + + [HttpGet("{storeId}/checkout")] + public IActionResult CheckoutAppearance() + { + var storeBlob = CurrentStore.GetStoreBlob(); + var vm = new CheckoutAppearanceViewModel(); + SetCryptoCurrencies(vm, CurrentStore); + vm.PaymentMethodCriteria = CurrentStore.GetPaymentMethodConfigs(_handlers) + .Where(s => !storeBlob.GetExcludedPaymentMethods().Match(s.Key) && s.Value is not LNURLPaymentMethodConfig) + .Select(c => + { + var pmi = c.Key; + var existing = storeBlob.PaymentMethodCriteria.SingleOrDefault(criteria => + criteria.PaymentMethod == pmi); + return existing is null + ? new PaymentMethodCriteriaViewModel { PaymentMethod = pmi.ToString(), Value = "" } + : new PaymentMethodCriteriaViewModel + { + PaymentMethod = existing.PaymentMethod.ToString(), + Type = existing.Above + ? PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan + : PaymentMethodCriteriaViewModel.CriteriaType.LessThan, + Value = existing.Value?.ToString() ?? "" + }; + }).ToList(); + + vm.UseClassicCheckout = storeBlob.CheckoutType == Client.Models.CheckoutType.V1; + vm.CelebratePayment = storeBlob.CelebratePayment; + vm.PlaySoundOnPayment = storeBlob.PlaySoundOnPayment; + vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback; + vm.ShowPayInWalletButton = storeBlob.ShowPayInWalletButton; + vm.ShowStoreHeader = storeBlob.ShowStoreHeader; + vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi; + vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail; + vm.LazyPaymentMethods = storeBlob.LazyPaymentMethods; + vm.RedirectAutomatically = storeBlob.RedirectAutomatically; + vm.CustomCSS = storeBlob.CustomCSS; + vm.CustomLogo = storeBlob.CustomLogo; + vm.SoundFileId = storeBlob.SoundFileId; + vm.HtmlTitle = storeBlob.HtmlTitle; + vm.SupportUrl = storeBlob.StoreSupportUrl; + vm.DisplayExpirationTimer = (int)storeBlob.DisplayExpirationTimer.TotalMinutes; + vm.ReceiptOptions = CheckoutAppearanceViewModel.ReceiptOptionsViewModel.Create(storeBlob.ReceiptOptions); + vm.AutoDetectLanguage = storeBlob.AutoDetectLanguage; + vm.SetLanguages(_LangService, storeBlob.DefaultLang); + + return View(vm); + } + + [HttpPost("{storeId}/checkout")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task CheckoutAppearance(CheckoutAppearanceViewModel model, [FromForm] bool RemoveSoundFile = false) + { + bool needUpdate = false; + var blob = CurrentStore.GetStoreBlob(); + var defaultPaymentMethodId = model.DefaultPaymentMethod == null ? null : PaymentMethodId.Parse(model.DefaultPaymentMethod); + if (CurrentStore.GetDefaultPaymentId() != defaultPaymentMethodId) + { + needUpdate = true; + CurrentStore.SetDefaultPaymentId(defaultPaymentMethodId); + } + SetCryptoCurrencies(model, CurrentStore); + model.SetLanguages(_LangService, model.DefaultLang); + model.PaymentMethodCriteria ??= new List(); + for (var index = 0; index < model.PaymentMethodCriteria.Count; index++) + { + var methodCriterion = model.PaymentMethodCriteria[index]; + if (!string.IsNullOrWhiteSpace(methodCriterion.Value)) + { + if (!CurrencyValue.TryParse(methodCriterion.Value, out _)) + { + model.AddModelError(viewModel => viewModel.PaymentMethodCriteria[index].Value, + $"{methodCriterion.PaymentMethod}: Invalid format. Make sure to enter a valid amount and currency code. Examples: '5 USD', '0.001 BTC'", this); + } + } + } + + var userId = GetUserId(); + if (userId is null) + return NotFound(); + + if (model.SoundFile != null) + { + if (model.SoundFile.Length > 1_000_000) + { + ModelState.AddModelError(nameof(model.SoundFile), "The uploaded sound file should be less than 1MB"); + } + else if (!model.SoundFile.ContentType.StartsWith("audio/", StringComparison.InvariantCulture)) + { + ModelState.AddModelError(nameof(model.SoundFile), "The uploaded sound file needs to be an audio file"); + } + else + { + var formFile = await model.SoundFile.Bufferize(); + if (!FileTypeDetector.IsAudio(formFile.Buffer, formFile.FileName)) + { + ModelState.AddModelError(nameof(model.SoundFile), "The uploaded sound file needs to be an audio file"); + } + else + { + model.SoundFile = formFile; + // delete existing file + if (!string.IsNullOrEmpty(blob.SoundFileId)) + { + await _fileService.RemoveFile(blob.SoundFileId, userId); + } + + // add new file + try + { + var storedFile = await _fileService.AddFile(model.SoundFile, userId); + blob.SoundFileId = storedFile.Id; + needUpdate = true; + } + catch (Exception e) + { + ModelState.AddModelError(nameof(model.SoundFile), $"Could not save sound: {e.Message}"); + } + } + } + } + else if (RemoveSoundFile && !string.IsNullOrEmpty(blob.SoundFileId)) + { + await _fileService.RemoveFile(blob.SoundFileId, userId); + blob.SoundFileId = null; + needUpdate = true; + } + + if (!ModelState.IsValid) + { + return View(model); + } + + // Payment criteria for Off-Chain should also affect LNUrl + foreach (var newCriteria in model.PaymentMethodCriteria.ToList()) + { + var paymentMethodId = PaymentMethodId.Parse(newCriteria.PaymentMethod); + if (_handlers.TryGet(paymentMethodId) is LightningLikePaymentHandler h) + model.PaymentMethodCriteria.Add(new PaymentMethodCriteriaViewModel() + { + PaymentMethod = PaymentTypes.LNURL.GetPaymentMethodId(h.Network.CryptoCode).ToString(), + Type = newCriteria.Type, + Value = newCriteria.Value + }); + // Should not be able to set LNUrlPay criteria directly in UI + if (_handlers.TryGet(paymentMethodId) is LNURLPayPaymentHandler) + model.PaymentMethodCriteria.Remove(newCriteria); + } + blob.PaymentMethodCriteria ??= new List(); + foreach (var newCriteria in model.PaymentMethodCriteria) + { + var paymentMethodId = PaymentMethodId.Parse(newCriteria.PaymentMethod); + var existingCriteria = blob.PaymentMethodCriteria.FirstOrDefault(c => c.PaymentMethod == paymentMethodId); + if (existingCriteria != null) + blob.PaymentMethodCriteria.Remove(existingCriteria); + CurrencyValue.TryParse(newCriteria.Value, out var cv); + blob.PaymentMethodCriteria.Add(new PaymentMethodCriteria() + { + Above = newCriteria.Type == PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan, + Value = cv, + PaymentMethod = paymentMethodId + }); + } + + blob.ShowPayInWalletButton = model.ShowPayInWalletButton; + blob.ShowStoreHeader = model.ShowStoreHeader; + blob.CheckoutType = model.UseClassicCheckout ? Client.Models.CheckoutType.V1 : Client.Models.CheckoutType.V2; + blob.CelebratePayment = model.CelebratePayment; + blob.PlaySoundOnPayment = model.PlaySoundOnPayment; + blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback; + blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi; + blob.RequiresRefundEmail = model.RequiresRefundEmail; + blob.LazyPaymentMethods = model.LazyPaymentMethods; + blob.RedirectAutomatically = model.RedirectAutomatically; + blob.ReceiptOptions = model.ReceiptOptions.ToDTO(); + blob.CustomLogo = model.CustomLogo; + blob.CustomCSS = model.CustomCSS; + blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle; + blob.StoreSupportUrl = string.IsNullOrWhiteSpace(model.SupportUrl) ? null : model.SupportUrl.IsValidEmail() ? $"mailto:{model.SupportUrl}" : model.SupportUrl; + blob.DisplayExpirationTimer = TimeSpan.FromMinutes(model.DisplayExpirationTimer); + blob.AutoDetectLanguage = model.AutoDetectLanguage; + blob.DefaultLang = model.DefaultLang; + blob.NormalizeToRelativeLinks(Request); + if (CurrentStore.SetStoreBlob(blob)) + { + needUpdate = true; + } + if (needUpdate) + { + await _Repo.UpdateStore(CurrentStore); + TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated"; + } + + return RedirectToAction(nameof(CheckoutAppearance), new + { + storeId = CurrentStore.Id + }); + } + + void SetCryptoCurrencies(CheckoutAppearanceViewModel vm, Data.StoreData storeData) + { + var choices = GetEnabledPaymentMethodChoices(storeData); + var chosen = GetDefaultPaymentMethodChoice(storeData); + + vm.PaymentMethods = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen?.Value); + vm.DefaultPaymentMethod = chosen?.Value; + } + + PaymentMethodOptionViewModel.Format? GetDefaultPaymentMethodChoice(StoreData storeData) + { + var enabled = storeData.GetEnabledPaymentIds(); + var defaultPaymentId = storeData.GetDefaultPaymentId(); + var defaultChoice = defaultPaymentId is not null ? defaultPaymentId.FindNearest(enabled) : null; + if (defaultChoice is null) + { + defaultChoice = enabled.FirstOrDefault(e => e == PaymentTypes.CHAIN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode)) ?? + enabled.FirstOrDefault(e => e == PaymentTypes.LN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode)) ?? + enabled.FirstOrDefault(); + } + var choices = GetEnabledPaymentMethodChoices(storeData); + + return defaultChoice is null ? null : choices.FirstOrDefault(c => defaultChoice.ToString().Equals(c.Value, StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/BTCPayServer/Controllers/UIStoresController.Tokens.cs b/BTCPayServer/Controllers/UIStoresController.Tokens.cs new file mode 100644 index 000000000..8d677f993 --- /dev/null +++ b/BTCPayServer/Controllers/UIStoresController.Tokens.cs @@ -0,0 +1,270 @@ +#nullable enable +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Client; +using BTCPayServer.Data; +using BTCPayServer.Models; +using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Security.Bitpay; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using NBitcoin; +using NBitcoin.DataEncoders; + +namespace BTCPayServer.Controllers +{ + public partial class UIStoresController + { + [HttpGet("{storeId}/tokens")] + [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task ListTokens() + { + var model = new TokensViewModel(); + var tokens = await _TokenRepository.GetTokensByStoreIdAsync(CurrentStore.Id); + model.StoreNotConfigured = StoreNotConfigured; + model.Tokens = tokens.Select(t => new TokenViewModel() + { + Label = t.Label, + SIN = t.SIN, + Id = t.Value + }).ToArray(); + + model.ApiKey = (await _TokenRepository.GetLegacyAPIKeys(CurrentStore.Id)).FirstOrDefault(); + if (model.ApiKey == null) + model.EncodedApiKey = "*API Key*"; + else + model.EncodedApiKey = Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(model.ApiKey)); + return View(model); + } + + [HttpGet("{storeId}/tokens/{tokenId}/revoke")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task RevokeToken(string tokenId) + { + var token = await _TokenRepository.GetToken(tokenId); + if (token == null || token.StoreId != CurrentStore.Id) + return NotFound(); + return View("Confirm", new ConfirmModel("Revoke the token", $"The access token with the label {Html.Encode(token.Label)} will be revoked. Do you wish to continue?", "Revoke")); + } + + [HttpPost("{storeId}/tokens/{tokenId}/revoke")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task RevokeTokenConfirm(string tokenId) + { + var token = await _TokenRepository.GetToken(tokenId); + if (token == null || + token.StoreId != CurrentStore.Id || + !await _TokenRepository.DeleteToken(tokenId)) + TempData[WellKnownTempData.ErrorMessage] = "Failure to revoke this token."; + else + TempData[WellKnownTempData.SuccessMessage] = "Token revoked"; + return RedirectToAction(nameof(ListTokens), new { storeId = token?.StoreId }); + } + + [HttpGet("{storeId}/tokens/{tokenId}")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task ShowToken(string tokenId) + { + var token = await _TokenRepository.GetToken(tokenId); + if (token == null || token.StoreId != CurrentStore.Id) + return NotFound(); + return View(token); + } + + [HttpGet("{storeId}/tokens/create")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public IActionResult CreateToken(string storeId) + { + var model = new CreateTokenViewModel(); + ViewBag.HidePublicKey = storeId == null; + ViewBag.ShowStores = storeId == null; + ViewBag.ShowMenu = storeId != null; + model.StoreId = storeId; + return View(model); + } + + [HttpPost("{storeId}/tokens/create")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task CreateToken(string storeId, CreateTokenViewModel model) + { + if (!ModelState.IsValid) + { + return View(nameof(CreateToken), model); + } + model.Label = model.Label ?? string.Empty; + var userId = GetUserId(); + if (userId == null) + return Challenge(AuthenticationSchemes.Cookie); + var store = model.StoreId switch + { + null => CurrentStore, + _ => await _Repo.FindStore(storeId, userId) + }; + if (store == null) + return Challenge(AuthenticationSchemes.Cookie); + var tokenRequest = new TokenRequest() + { + Label = model.Label, + Id = model.PublicKey == null ? null : NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(new PubKey(model.PublicKey).Compress()) + }; + + string? pairingCode = null; + if (model.PublicKey == null) + { + tokenRequest.PairingCode = await _TokenRepository.CreatePairingCodeAsync(); + await _TokenRepository.UpdatePairingCode(new PairingCodeEntity() + { + Id = tokenRequest.PairingCode, + Label = model.Label, + }); + await _TokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, store.Id); + pairingCode = tokenRequest.PairingCode; + } + else + { + pairingCode = (await _TokenController.Tokens(tokenRequest)).Data[0].PairingCode; + } + + GeneratedPairingCode = pairingCode; + return RedirectToAction(nameof(RequestPairing), new + { + pairingCode, + selectedStore = storeId + }); + } + + [HttpGet("/api-tokens")] + [AllowAnonymous] + public async Task CreateToken() + { + var userId = GetUserId(); + if (string.IsNullOrWhiteSpace(userId)) + return Challenge(AuthenticationSchemes.Cookie); + var model = new CreateTokenViewModel(); + ViewBag.HidePublicKey = true; + ViewBag.ShowStores = true; + ViewBag.ShowMenu = false; + var stores = (await _Repo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray(); + + model.Stores = new SelectList(stores, nameof(CurrentStore.Id), nameof(CurrentStore.StoreName)); + if (!model.Stores.Any()) + { + TempData[WellKnownTempData.ErrorMessage] = "You need to be owner of at least one store before pairing"; + return RedirectToAction(nameof(UIHomeController.Index), "UIHome"); + } + return View(model); + } + + [HttpPost("/api-tokens")] + [AllowAnonymous] + public Task CreateToken2(CreateTokenViewModel model) + { + return CreateToken(model.StoreId, model); + } + + [HttpPost("{storeId}/tokens/apikey")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task GenerateAPIKey(string storeId, string command = "") + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + if (command == "revoke") + { + await _TokenRepository.RevokeLegacyAPIKeys(CurrentStore.Id); + TempData[WellKnownTempData.SuccessMessage] = "API Key revoked"; + } + else + { + await _TokenRepository.GenerateLegacyAPIKey(CurrentStore.Id); + TempData[WellKnownTempData.SuccessMessage] = "API Key re-generated"; + } + + return RedirectToAction(nameof(ListTokens), new + { + storeId + }); + } + + [HttpGet("/api-access-request")] + [AllowAnonymous] + public async Task RequestPairing(string pairingCode, string? selectedStore = null) + { + var userId = GetUserId(); + if (userId == null) + return Challenge(AuthenticationSchemes.Cookie); + + if (pairingCode == null) + return NotFound(); + + if (selectedStore != null) + { + var store = await _Repo.FindStore(selectedStore, userId); + if (store == null) + return NotFound(); + HttpContext.SetStoreData(store); + } + + var pairing = await _TokenRepository.GetPairingAsync(pairingCode); + if (pairing == null) + { + TempData[WellKnownTempData.ErrorMessage] = "Unknown pairing code"; + return RedirectToAction(nameof(UIHomeController.Index), "UIHome"); + } + + var stores = (await _Repo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray(); + return View(new PairingModel + { + Id = pairing.Id, + Label = pairing.Label, + SIN = pairing.SIN ?? "Server-Initiated Pairing", + StoreId = selectedStore ?? stores.FirstOrDefault()?.Id, + Stores = stores.Select(s => new PairingModel.StoreViewModel + { + Id = s.Id, + Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName + }).ToArray() + }); + } + + [HttpPost("/api-access-request")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task Pair(string pairingCode, string storeId) + { + if (pairingCode == null) + return NotFound(); + var store = CurrentStore; + var pairing = await _TokenRepository.GetPairingAsync(pairingCode); + if (store == null || pairing == null) + return NotFound(); + + var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id); + if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial) + { + var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods(); + StoreNotConfigured = !store.GetPaymentMethodConfigs(_handlers) + .Where(p => !excludeFilter.Match(p.Key)) + .Any(); + TempData[WellKnownTempData.SuccessMessage] = "Pairing is successful"; + if (pairingResult == PairingResult.Partial) + TempData[WellKnownTempData.SuccessMessage] = "Server initiated pairing code: " + pairingCode; + return RedirectToAction(nameof(ListTokens), new + { + storeId = store.Id, + pairingCode = pairingCode + }); + } + else + { + TempData[WellKnownTempData.ErrorMessage] = $"Pairing failed ({pairingResult})"; + return RedirectToAction(nameof(ListTokens), new + { + storeId = store.Id + }); + } + } + } +} diff --git a/BTCPayServer/Controllers/UIStoresController.Users.cs b/BTCPayServer/Controllers/UIStoresController.Users.cs index bcdd7d0db..467327e5f 100644 --- a/BTCPayServer/Controllers/UIStoresController.Users.cs +++ b/BTCPayServer/Controllers/UIStoresController.Users.cs @@ -1,42 +1,18 @@ #nullable enable using System; -using System.Collections.Generic; -using System.Globalization; using System.Linq; -using System.Text.RegularExpressions; -using System.Threading; using System.Threading.Tasks; using BTCPayServer.Abstractions.Constants; -using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Abstractions.Models; using BTCPayServer.Client; -using BTCPayServer.Configuration; using BTCPayServer.Data; using BTCPayServer.Events; -using BTCPayServer.HostedServices.Webhooks; -using BTCPayServer.Models; using BTCPayServer.Models.StoreViewModels; -using BTCPayServer.Payments; -using BTCPayServer.Payments.Lightning; -using BTCPayServer.Rating; -using BTCPayServer.Security.Bitpay; -using BTCPayServer.Services; -using BTCPayServer.Services.Apps; -using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Mails; -using BTCPayServer.Services.Rates; using BTCPayServer.Services.Stores; -using BTCPayServer.Services.Wallets; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.Extensions.Options; -using NBitcoin; -using NBitcoin.DataEncoders; -using StoreData = BTCPayServer.Data.StoreData; namespace BTCPayServer.Controllers { diff --git a/BTCPayServer/Controllers/UIStoresController.cs b/BTCPayServer/Controllers/UIStoresController.cs index 81c7c2cd6..b49b4a6dc 100644 --- a/BTCPayServer/Controllers/UIStoresController.cs +++ b/BTCPayServer/Controllers/UIStoresController.cs @@ -1,26 +1,13 @@ #nullable enable -using System; -using System.Collections.Generic; -using System.Globalization; using System.Linq; -using System.Text.RegularExpressions; -using System.Threading; using System.Threading.Tasks; using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Contracts; -using BTCPayServer.Abstractions.Extensions; -using BTCPayServer.Abstractions.Models; using BTCPayServer.Client; using BTCPayServer.Configuration; using BTCPayServer.Data; -using BTCPayServer.Events; using BTCPayServer.HostedServices.Webhooks; -using BTCPayServer.Models; using BTCPayServer.Models.StoreViewModels; -using BTCPayServer.Payments; -using BTCPayServer.Payments.Bitcoin; -using BTCPayServer.Payments.Lightning; -using BTCPayServer.Rating; using BTCPayServer.Security.Bitpay; using BTCPayServer.Services; using BTCPayServer.Services.Apps; @@ -35,8 +22,6 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.Extensions.Options; -using NBitcoin; -using NBitcoin.DataEncoders; using StoreData = BTCPayServer.Data.StoreData; namespace BTCPayServer.Controllers @@ -166,228 +151,7 @@ namespace BTCPayServer.Controllers return Forbid(); } - public StoreData? CurrentStore => HttpContext.GetStoreData(); - - [HttpGet("{storeId}/rates")] - public IActionResult Rates() - { - var exchanges = GetSupportedExchanges(); - var storeBlob = CurrentStore.GetStoreBlob(); - var vm = new RatesViewModel(); - vm.SetExchangeRates(exchanges, storeBlob.PreferredExchange ?? storeBlob.GetRecommendedExchange()); - vm.Spread = (double)(storeBlob.Spread * 100m); - vm.StoreId = CurrentStore.Id; - vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString(); - vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString(); - vm.AvailableExchanges = exchanges; - vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString(); - vm.ShowScripting = storeBlob.RateScripting; - return View(vm); - } - - [HttpPost("{storeId}/rates")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task Rates(RatesViewModel model, string? command = null, string? storeId = null, CancellationToken cancellationToken = default) - { - if (command == "scripting-on") - { - return RedirectToAction(nameof(ShowRateRules), new { scripting = true, storeId = model.StoreId }); - } - else if (command == "scripting-off") - { - return RedirectToAction(nameof(ShowRateRules), new { scripting = false, storeId = model.StoreId }); - } - - var exchanges = GetSupportedExchanges(); - model.SetExchangeRates(exchanges, model.PreferredExchange ?? this.HttpContext.GetStoreData().GetStoreBlob().GetRecommendedExchange()); - model.StoreId = storeId ?? model.StoreId; - CurrencyPair[]? currencyPairs = null; - try - { - currencyPairs = model.DefaultCurrencyPairs? - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(p => CurrencyPair.Parse(p)) - .ToArray(); - } - catch - { - ModelState.AddModelError(nameof(model.DefaultCurrencyPairs), "Invalid currency pairs (should be for example: BTC_USD,BTC_CAD,BTC_JPY)"); - } - if (!ModelState.IsValid) - { - return View(model); - } - if (model.PreferredExchange != null) - model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant(); - - var blob = CurrentStore.GetStoreBlob(); - model.DefaultScript = blob.GetDefaultRateRules(_NetworkProvider).ToString(); - model.AvailableExchanges = exchanges; - - blob.PreferredExchange = model.PreferredExchange; - blob.Spread = (decimal)model.Spread / 100.0m; - blob.DefaultCurrencyPairs = currencyPairs; - if (!model.ShowScripting) - { - if (!exchanges.Any(provider => provider.Id.Equals(model.PreferredExchange, StringComparison.InvariantCultureIgnoreCase))) - { - ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})"); - return View(model); - } - } - RateRules? rules = null; - if (model.ShowScripting) - { - if (!RateRules.TryParse(model.Script, out rules, out var errors)) - { - errors = errors ?? new List(); - var errorString = String.Join(", ", errors.ToArray()); - ModelState.AddModelError(nameof(model.Script), $"Parsing error ({errorString})"); - return View(model); - } - else - { - blob.RateScript = rules.ToString(); - ModelState.Remove(nameof(model.Script)); - model.Script = blob.RateScript; - } - } - rules = blob.GetRateRules(_NetworkProvider); - - if (command == "Test") - { - if (string.IsNullOrWhiteSpace(model.ScriptTest)) - { - ModelState.AddModelError(nameof(model.ScriptTest), "Fill out currency pair to test for (like BTC_USD,BTC_CAD)"); - return View(model); - } - var splitted = model.ScriptTest.Split(',', StringSplitOptions.RemoveEmptyEntries); - - var pairs = new List(); - foreach (var pair in splitted) - { - if (!CurrencyPair.TryParse(pair, out var currencyPair)) - { - ModelState.AddModelError(nameof(model.ScriptTest), $"Invalid currency pair '{pair}' (it should be formatted like BTC_USD,BTC_CAD)"); - return View(model); - } - pairs.Add(currencyPair); - } - - var fetchs = _RateFactory.FetchRates(pairs.ToHashSet(), rules, cancellationToken); - var testResults = new List(); - foreach (var fetch in fetchs) - { - var testResult = await (fetch.Value); - testResults.Add(new RatesViewModel.TestResultViewModel() - { - CurrencyPair = fetch.Key.ToString(), - Error = testResult.Errors.Count != 0, - Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.BidAsk.Bid.ToString(CultureInfo.InvariantCulture) - : testResult.EvaluatedRule - }); - } - model.TestRateRules = testResults; - return View(model); - } - else // command == Save - { - if (CurrentStore.SetStoreBlob(blob)) - { - await _Repo.UpdateStore(CurrentStore); - TempData[WellKnownTempData.SuccessMessage] = "Rate settings updated"; - } - return RedirectToAction(nameof(Rates), new - { - storeId = CurrentStore.Id - }); - } - } - - [HttpGet("{storeId}/rates/confirm")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public IActionResult ShowRateRules(bool scripting) - { - return View("Confirm", new ConfirmModel - { - Action = "Continue", - Title = "Rate rule scripting", - Description = scripting ? - "This action will modify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)" - : "This action will delete your rate script. Are you sure to turn off rate rules scripting?", - ButtonClass = scripting ? "btn-primary" : "btn-danger" - }); - } - - [HttpPost("{storeId}/rates/confirm")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task ShowRateRulesPost(bool scripting) - { - var blob = CurrentStore.GetStoreBlob(); - blob.RateScripting = scripting; - blob.RateScript = blob.GetDefaultRateRules(_NetworkProvider).ToString(); - CurrentStore.SetStoreBlob(blob); - await _Repo.UpdateStore(CurrentStore); - TempData[WellKnownTempData.SuccessMessage] = "Rate rules scripting " + (scripting ? "activated" : "deactivated"); - return RedirectToAction(nameof(Rates), new { storeId = CurrentStore.Id }); - } - - [HttpGet("{storeId}/checkout")] - public IActionResult CheckoutAppearance() - { - var storeBlob = CurrentStore.GetStoreBlob(); - var vm = new CheckoutAppearanceViewModel(); - SetCryptoCurrencies(vm, CurrentStore); - vm.PaymentMethodCriteria = CurrentStore.GetPaymentMethodConfigs(_handlers) - .Where(s => !storeBlob.GetExcludedPaymentMethods().Match(s.Key) && s.Value is not LNURLPaymentMethodConfig) - .Select(c => - { - var pmi = c.Key; - var existing = storeBlob.PaymentMethodCriteria.SingleOrDefault(criteria => - criteria.PaymentMethod == pmi); - return existing is null - ? new PaymentMethodCriteriaViewModel { PaymentMethod = pmi.ToString(), Value = "" } - : new PaymentMethodCriteriaViewModel - { - PaymentMethod = existing.PaymentMethod.ToString(), - Type = existing.Above - ? PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan - : PaymentMethodCriteriaViewModel.CriteriaType.LessThan, - Value = existing.Value?.ToString() ?? "" - }; - }).ToList(); - - vm.UseClassicCheckout = storeBlob.CheckoutType == Client.Models.CheckoutType.V1; - vm.CelebratePayment = storeBlob.CelebratePayment; - vm.PlaySoundOnPayment = storeBlob.PlaySoundOnPayment; - vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback; - vm.ShowPayInWalletButton = storeBlob.ShowPayInWalletButton; - vm.ShowStoreHeader = storeBlob.ShowStoreHeader; - vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi; - vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail; - vm.LazyPaymentMethods = storeBlob.LazyPaymentMethods; - vm.RedirectAutomatically = storeBlob.RedirectAutomatically; - vm.CustomCSS = storeBlob.CustomCSS; - vm.CustomLogo = storeBlob.CustomLogo; - vm.SoundFileId = storeBlob.SoundFileId; - vm.HtmlTitle = storeBlob.HtmlTitle; - vm.SupportUrl = storeBlob.StoreSupportUrl; - vm.DisplayExpirationTimer = (int)storeBlob.DisplayExpirationTimer.TotalMinutes; - vm.ReceiptOptions = CheckoutAppearanceViewModel.ReceiptOptionsViewModel.Create(storeBlob.ReceiptOptions); - vm.AutoDetectLanguage = storeBlob.AutoDetectLanguage; - vm.SetLanguages(_LangService, storeBlob.DefaultLang); - - return View(vm); - } - - void SetCryptoCurrencies(CheckoutAppearanceViewModel vm, Data.StoreData storeData) - { - var choices = GetEnabledPaymentMethodChoices(storeData); - var chosen = GetDefaultPaymentMethodChoice(storeData); - - vm.PaymentMethods = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen?.Value); - vm.DefaultPaymentMethod = chosen?.Value; - } + public StoreData CurrentStore => HttpContext.GetStoreData(); public PaymentMethodOptionViewModel.Format[] GetEnabledPaymentMethodChoices(StoreData storeData) { @@ -403,707 +167,6 @@ namespace BTCPayServer.Controllers }).ToArray(); } - PaymentMethodOptionViewModel.Format? GetDefaultPaymentMethodChoice(StoreData storeData) - { - var enabled = storeData.GetEnabledPaymentIds(); - var defaultPaymentId = storeData.GetDefaultPaymentId(); - var defaultChoice = defaultPaymentId is not null ? defaultPaymentId.FindNearest(enabled) : null; - if (defaultChoice is null) - { - defaultChoice = enabled.FirstOrDefault(e => e == PaymentTypes.CHAIN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode)) ?? - enabled.FirstOrDefault(e => e == PaymentTypes.LN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode)) ?? - enabled.FirstOrDefault(); - } - var choices = GetEnabledPaymentMethodChoices(storeData); - - return defaultChoice is null ? null : choices.FirstOrDefault(c => defaultChoice.ToString().Equals(c.Value, StringComparison.OrdinalIgnoreCase)); - } - - [HttpPost("{storeId}/checkout")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task CheckoutAppearance(CheckoutAppearanceViewModel model, [FromForm] bool RemoveSoundFile = false) - { - bool needUpdate = false; - var blob = CurrentStore.GetStoreBlob(); - var defaultPaymentMethodId = model.DefaultPaymentMethod == null ? null : PaymentMethodId.Parse(model.DefaultPaymentMethod); - if (CurrentStore.GetDefaultPaymentId() != defaultPaymentMethodId) - { - needUpdate = true; - CurrentStore.SetDefaultPaymentId(defaultPaymentMethodId); - } - SetCryptoCurrencies(model, CurrentStore); - model.SetLanguages(_LangService, model.DefaultLang); - model.PaymentMethodCriteria ??= new List(); - for (var index = 0; index < model.PaymentMethodCriteria.Count; index++) - { - var methodCriterion = model.PaymentMethodCriteria[index]; - if (!string.IsNullOrWhiteSpace(methodCriterion.Value)) - { - if (!CurrencyValue.TryParse(methodCriterion.Value, out _)) - { - model.AddModelError(viewModel => viewModel.PaymentMethodCriteria[index].Value, - $"{methodCriterion.PaymentMethod}: Invalid format. Make sure to enter a valid amount and currency code. Examples: '5 USD', '0.001 BTC'", this); - } - } - } - - var userId = GetUserId(); - if (userId is null) - return NotFound(); - - if (model.SoundFile != null) - { - if (model.SoundFile.Length > 1_000_000) - { - ModelState.AddModelError(nameof(model.SoundFile), "The uploaded sound file should be less than 1MB"); - } - else if (!model.SoundFile.ContentType.StartsWith("audio/", StringComparison.InvariantCulture)) - { - ModelState.AddModelError(nameof(model.SoundFile), "The uploaded sound file needs to be an audio file"); - } - else - { - var formFile = await model.SoundFile.Bufferize(); - if (!FileTypeDetector.IsAudio(formFile.Buffer, formFile.FileName)) - { - ModelState.AddModelError(nameof(model.SoundFile), "The uploaded sound file needs to be an audio file"); - } - else - { - model.SoundFile = formFile; - // delete existing file - if (!string.IsNullOrEmpty(blob.SoundFileId)) - { - await _fileService.RemoveFile(blob.SoundFileId, userId); - } - - // add new file - try - { - var storedFile = await _fileService.AddFile(model.SoundFile, userId); - blob.SoundFileId = storedFile.Id; - needUpdate = true; - } - catch (Exception e) - { - ModelState.AddModelError(nameof(model.SoundFile), $"Could not save sound: {e.Message}"); - } - } - } - } - else if (RemoveSoundFile && !string.IsNullOrEmpty(blob.SoundFileId)) - { - await _fileService.RemoveFile(blob.SoundFileId, userId); - blob.SoundFileId = null; - needUpdate = true; - } - - if (!ModelState.IsValid) - { - return View(model); - } - - // Payment criteria for Off-Chain should also affect LNUrl - foreach (var newCriteria in model.PaymentMethodCriteria.ToList()) - { - var paymentMethodId = PaymentMethodId.Parse(newCriteria.PaymentMethod); - if (_handlers.TryGet(paymentMethodId) is LightningLikePaymentHandler h) - model.PaymentMethodCriteria.Add(new PaymentMethodCriteriaViewModel() - { - PaymentMethod = PaymentTypes.LNURL.GetPaymentMethodId(h.Network.CryptoCode).ToString(), - Type = newCriteria.Type, - Value = newCriteria.Value - }); - // Should not be able to set LNUrlPay criteria directly in UI - if (_handlers.TryGet(paymentMethodId) is LNURLPayPaymentHandler) - model.PaymentMethodCriteria.Remove(newCriteria); - } - blob.PaymentMethodCriteria ??= new List(); - foreach (var newCriteria in model.PaymentMethodCriteria) - { - var paymentMethodId = PaymentMethodId.Parse(newCriteria.PaymentMethod); - var existingCriteria = blob.PaymentMethodCriteria.FirstOrDefault(c => c.PaymentMethod == paymentMethodId); - if (existingCriteria != null) - blob.PaymentMethodCriteria.Remove(existingCriteria); - CurrencyValue.TryParse(newCriteria.Value, out var cv); - blob.PaymentMethodCriteria.Add(new PaymentMethodCriteria() - { - Above = newCriteria.Type == PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan, - Value = cv, - PaymentMethod = paymentMethodId - }); - } - - blob.ShowPayInWalletButton = model.ShowPayInWalletButton; - blob.ShowStoreHeader = model.ShowStoreHeader; - blob.CheckoutType = model.UseClassicCheckout ? Client.Models.CheckoutType.V1 : Client.Models.CheckoutType.V2; - blob.CelebratePayment = model.CelebratePayment; - blob.PlaySoundOnPayment = model.PlaySoundOnPayment; - blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback; - blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi; - blob.RequiresRefundEmail = model.RequiresRefundEmail; - blob.LazyPaymentMethods = model.LazyPaymentMethods; - blob.RedirectAutomatically = model.RedirectAutomatically; - blob.ReceiptOptions = model.ReceiptOptions.ToDTO(); - blob.CustomLogo = model.CustomLogo; - blob.CustomCSS = model.CustomCSS; - blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle; - blob.StoreSupportUrl = string.IsNullOrWhiteSpace(model.SupportUrl) ? null : model.SupportUrl.IsValidEmail() ? $"mailto:{model.SupportUrl}" : model.SupportUrl; - blob.DisplayExpirationTimer = TimeSpan.FromMinutes(model.DisplayExpirationTimer); - blob.AutoDetectLanguage = model.AutoDetectLanguage; - blob.DefaultLang = model.DefaultLang; - blob.NormalizeToRelativeLinks(Request); - if (CurrentStore.SetStoreBlob(blob)) - { - needUpdate = true; - } - if (needUpdate) - { - await _Repo.UpdateStore(CurrentStore); - TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated"; - } - - return RedirectToAction(nameof(CheckoutAppearance), new - { - storeId = CurrentStore.Id - }); - } - - internal void AddPaymentMethods(StoreData store, StoreBlob storeBlob, - out List derivationSchemes, out List lightningNodes) - { - var excludeFilters = storeBlob.GetExcludedPaymentMethods(); - var derivationByCryptoCode = - store - .GetPaymentMethodConfigs(_handlers) - .ToDictionary(c => ((IHasNetwork)_handlers[c.Key]).Network.CryptoCode, c => (DerivationSchemeSettings)c.Value); - - var lightningByCryptoCode = store - .GetPaymentMethodConfigs(_handlers) - .Where(c => c.Value is LightningPaymentMethodConfig) - .ToDictionary(c => ((IHasNetwork)_handlers[c.Key]).Network.CryptoCode, c => (LightningPaymentMethodConfig)c.Value); - - derivationSchemes = new List(); - lightningNodes = new List(); - - foreach (var handler in _handlers) - { - if (handler is BitcoinLikePaymentHandler { Network: var network }) - { - var strategy = derivationByCryptoCode.TryGet(network.CryptoCode); - var value = strategy?.ToPrettyString() ?? string.Empty; - - derivationSchemes.Add(new StoreDerivationScheme - { - Crypto = network.CryptoCode, - PaymentMethodId = handler.PaymentMethodId, - WalletSupported = network.WalletSupported, - Value = value, - WalletId = new WalletId(store.Id, network.CryptoCode), - Enabled = !excludeFilters.Match(handler.PaymentMethodId) && strategy != null, -#if ALTCOINS - Collapsed = network is Plugins.Altcoins.ElementsBTCPayNetwork elementsBTCPayNetwork && elementsBTCPayNetwork.NetworkCryptoCode != elementsBTCPayNetwork.CryptoCode && string.IsNullOrEmpty(value) -#endif - }); - } - else if (handler is LightningLikePaymentHandler) - { - var lnNetwork = ((IHasNetwork)handler).Network; - var lightning = lightningByCryptoCode.TryGet(lnNetwork.CryptoCode); - var isEnabled = !excludeFilters.Match(handler.PaymentMethodId) && lightning != null; - lightningNodes.Add(new StoreLightningNode - { - CryptoCode = lnNetwork.CryptoCode, - PaymentMethodId = handler.PaymentMethodId, - Address = lightning?.GetDisplayableConnectionString(), - Enabled = isEnabled - }); - } - } - } - - [HttpGet("{storeId}/settings")] - public IActionResult GeneralSettings() - { - var store = HttpContext.GetStoreData(); - if (store == null) - return NotFound(); - - var storeBlob = store.GetStoreBlob(); - var vm = new GeneralSettingsViewModel - { - Id = store.Id, - StoreName = store.StoreName, - StoreWebsite = store.StoreWebsite, - LogoFileId = storeBlob.LogoFileId, - CssFileId = storeBlob.CssFileId, - BrandColor = storeBlob.BrandColor, - NetworkFeeMode = storeBlob.NetworkFeeMode, - AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice, - PaymentTolerance = storeBlob.PaymentTolerance, - InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes, - DefaultCurrency = storeBlob.DefaultCurrency, - BOLT11Expiration = (long)storeBlob.RefundBOLT11Expiration.TotalDays, - Archived = store.Archived, - CanDelete = _Repo.CanDeleteStores() - }; - - return View(vm); - } - - [HttpPost("{storeId}/settings")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task GeneralSettings( - GeneralSettingsViewModel model, - [FromForm] bool RemoveLogoFile = false, - [FromForm] bool RemoveCssFile = false) - { - bool needUpdate = false; - if (CurrentStore.StoreName != model.StoreName) - { - needUpdate = true; - CurrentStore.StoreName = model.StoreName; - } - - if (CurrentStore.StoreWebsite != model.StoreWebsite) - { - needUpdate = true; - CurrentStore.StoreWebsite = model.StoreWebsite; - } - - var blob = CurrentStore.GetStoreBlob(); - blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice; - blob.NetworkFeeMode = model.NetworkFeeMode; - blob.PaymentTolerance = model.PaymentTolerance; - blob.DefaultCurrency = model.DefaultCurrency; - blob.InvoiceExpiration = TimeSpan.FromMinutes(model.InvoiceExpiration); - blob.RefundBOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration); - if (!string.IsNullOrEmpty(model.BrandColor) && !ColorPalette.IsValid(model.BrandColor)) - { - ModelState.AddModelError(nameof(model.BrandColor), "Invalid color"); - return View(model); - } - blob.BrandColor = model.BrandColor; - - var userId = GetUserId(); - if (userId is null) - return NotFound(); - - if (model.LogoFile != null) - { - if (model.LogoFile.Length > 1_000_000) - { - ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file should be less than 1MB"); - } - else if (!model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture)) - { - ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file needs to be an image"); - } - else - { - var formFile = await model.LogoFile.Bufferize(); - if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName)) - { - ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file needs to be an image"); - } - else - { - model.LogoFile = formFile; - // delete existing file - if (!string.IsNullOrEmpty(blob.LogoFileId)) - { - await _fileService.RemoveFile(blob.LogoFileId, userId); - } - // add new image - try - { - var storedFile = await _fileService.AddFile(model.LogoFile, userId); - blob.LogoFileId = storedFile.Id; - } - catch (Exception e) - { - ModelState.AddModelError(nameof(model.LogoFile), $"Could not save logo: {e.Message}"); - } - } - } - } - else if (RemoveLogoFile && !string.IsNullOrEmpty(blob.LogoFileId)) - { - await _fileService.RemoveFile(blob.LogoFileId, userId); - blob.LogoFileId = null; - needUpdate = true; - } - - if (model.CssFile != null) - { - if (model.CssFile.Length > 1_000_000) - { - ModelState.AddModelError(nameof(model.CssFile), "The uploaded file should be less than 1MB"); - } - else if (!model.CssFile.ContentType.Equals("text/css", StringComparison.InvariantCulture)) - { - ModelState.AddModelError(nameof(model.CssFile), "The uploaded file needs to be a CSS file"); - } - else if (!model.CssFile.FileName.EndsWith(".css", StringComparison.OrdinalIgnoreCase)) - { - ModelState.AddModelError(nameof(model.CssFile), "The uploaded file needs to be a CSS file"); - } - else - { - // delete existing file - if (!string.IsNullOrEmpty(blob.CssFileId)) - { - await _fileService.RemoveFile(blob.CssFileId, userId); - } - // add new file - try - { - var storedFile = await _fileService.AddFile(model.CssFile, userId); - blob.CssFileId = storedFile.Id; - } - catch (Exception e) - { - ModelState.AddModelError(nameof(model.CssFile), $"Could not save CSS file: {e.Message}"); - } - } - } - else if (RemoveCssFile && !string.IsNullOrEmpty(blob.CssFileId)) - { - await _fileService.RemoveFile(blob.CssFileId, userId); - blob.CssFileId = null; - needUpdate = true; - } - - if (CurrentStore.SetStoreBlob(blob)) - { - needUpdate = true; - } - - if (needUpdate) - { - await _Repo.UpdateStore(CurrentStore); - - TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated"; - } - - return RedirectToAction(nameof(GeneralSettings), new - { - storeId = CurrentStore.Id - }); - } - - [HttpPost("{storeId}/archive")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task ToggleArchive(string storeId) - { - CurrentStore.Archived = !CurrentStore.Archived; - await _Repo.UpdateStore(CurrentStore); - - TempData[WellKnownTempData.SuccessMessage] = CurrentStore.Archived - ? "The store has been archived and will no longer appear in the stores list by default." - : "The store has been unarchived and will appear in the stores list by default again."; - - return RedirectToAction(nameof(GeneralSettings), new - { - storeId = CurrentStore.Id - }); - } - - [HttpGet("{storeId}/delete")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public IActionResult DeleteStore(string storeId) - { - return View("Confirm", new ConfirmModel("Delete store", "The store will be permanently deleted. This action will also delete all invoices, apps and data associated with the store. Are you sure?", "Delete")); - } - - [HttpPost("{storeId}/delete")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task DeleteStorePost(string storeId) - { - await _Repo.DeleteStore(CurrentStore.Id); - TempData[WellKnownTempData.SuccessMessage] = "Store successfully deleted."; - return RedirectToAction(nameof(UIHomeController.Index), "UIHome"); - } - - private IEnumerable GetSupportedExchanges() - { - return _RateFactory.RateProviderFactory.AvailableRateProviders - .OrderBy(s => s.DisplayName, StringComparer.OrdinalIgnoreCase); - - } - - private DerivationSchemeSettings ParseDerivationStrategy(string derivationScheme, BTCPayNetwork network) - { - var parser = new DerivationSchemeParser(network); - var isOD = Regex.Match(derivationScheme, @"\(.*?\)"); - if (isOD.Success) - { - var derivationSchemeSettings = new DerivationSchemeSettings(); - var result = parser.ParseOutputDescriptor(derivationScheme); - derivationSchemeSettings.AccountOriginal = derivationScheme.Trim(); - derivationSchemeSettings.AccountDerivation = result.Item1; - derivationSchemeSettings.AccountKeySettings = result.Item2?.Select((path, i) => new AccountKeySettings() - { - RootFingerprint = path?.MasterFingerprint, - AccountKeyPath = path?.KeyPath, - AccountKey = result.Item1.GetExtPubKeys().ElementAt(i).GetWif(parser.Network) - }).ToArray() ?? new AccountKeySettings[result.Item1.GetExtPubKeys().Count()]; - return derivationSchemeSettings; - } - - var strategy = parser.Parse(derivationScheme); - return new DerivationSchemeSettings(strategy, network); - } - - [HttpGet("{storeId}/tokens")] - [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task ListTokens() - { - var model = new TokensViewModel(); - var tokens = await _TokenRepository.GetTokensByStoreIdAsync(CurrentStore.Id); - model.StoreNotConfigured = StoreNotConfigured; - model.Tokens = tokens.Select(t => new TokenViewModel() - { - Label = t.Label, - SIN = t.SIN, - Id = t.Value - }).ToArray(); - - model.ApiKey = (await _TokenRepository.GetLegacyAPIKeys(CurrentStore.Id)).FirstOrDefault(); - if (model.ApiKey == null) - model.EncodedApiKey = "*API Key*"; - else - model.EncodedApiKey = Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(model.ApiKey)); - return View(model); - } - - [HttpGet("{storeId}/tokens/{tokenId}/revoke")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task RevokeToken(string tokenId) - { - var token = await _TokenRepository.GetToken(tokenId); - if (token == null || token.StoreId != CurrentStore.Id) - return NotFound(); - return View("Confirm", new ConfirmModel("Revoke the token", $"The access token with the label {Html.Encode(token.Label)} will be revoked. Do you wish to continue?", "Revoke")); - } - - [HttpPost("{storeId}/tokens/{tokenId}/revoke")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task RevokeTokenConfirm(string tokenId) - { - var token = await _TokenRepository.GetToken(tokenId); - if (token == null || - token.StoreId != CurrentStore.Id || - !await _TokenRepository.DeleteToken(tokenId)) - TempData[WellKnownTempData.ErrorMessage] = "Failure to revoke this token."; - else - TempData[WellKnownTempData.SuccessMessage] = "Token revoked"; - return RedirectToAction(nameof(ListTokens), new { storeId = token?.StoreId }); - } - - [HttpGet("{storeId}/tokens/{tokenId}")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task ShowToken(string tokenId) - { - var token = await _TokenRepository.GetToken(tokenId); - if (token == null || token.StoreId != CurrentStore.Id) - return NotFound(); - return View(token); - } - - [HttpGet("{storeId}/tokens/create")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public IActionResult CreateToken(string storeId) - { - var model = new CreateTokenViewModel(); - ViewBag.HidePublicKey = storeId == null; - ViewBag.ShowStores = storeId == null; - ViewBag.ShowMenu = storeId != null; - model.StoreId = storeId; - return View(model); - } - - [HttpPost("{storeId}/tokens/create")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task CreateToken(string storeId, CreateTokenViewModel model) - { - if (!ModelState.IsValid) - { - return View(nameof(CreateToken), model); - } - model.Label = model.Label ?? String.Empty; - var userId = GetUserId(); - if (userId == null) - return Challenge(AuthenticationSchemes.Cookie); - var store = model.StoreId switch - { - null => CurrentStore, - _ => await _Repo.FindStore(storeId, userId) - }; - if (store == null) - return Challenge(AuthenticationSchemes.Cookie); - var tokenRequest = new TokenRequest() - { - Label = model.Label, - Id = model.PublicKey == null ? null : NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(new PubKey(model.PublicKey).Compress()) - }; - - string? pairingCode = null; - if (model.PublicKey == null) - { - tokenRequest.PairingCode = await _TokenRepository.CreatePairingCodeAsync(); - await _TokenRepository.UpdatePairingCode(new PairingCodeEntity() - { - Id = tokenRequest.PairingCode, - Label = model.Label, - }); - await _TokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, store.Id); - pairingCode = tokenRequest.PairingCode; - } - else - { - pairingCode = (await _TokenController.Tokens(tokenRequest)).Data[0].PairingCode; - } - - GeneratedPairingCode = pairingCode; - return RedirectToAction(nameof(RequestPairing), new - { - pairingCode, - selectedStore = storeId - }); - } - - [HttpGet("/api-tokens")] - [AllowAnonymous] - public async Task CreateToken() - { - var userId = GetUserId(); - if (string.IsNullOrWhiteSpace(userId)) - return Challenge(AuthenticationSchemes.Cookie); - var model = new CreateTokenViewModel(); - ViewBag.HidePublicKey = true; - ViewBag.ShowStores = true; - ViewBag.ShowMenu = false; - var stores = (await _Repo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray(); - - model.Stores = new SelectList(stores, nameof(CurrentStore.Id), nameof(CurrentStore.StoreName)); - if (!model.Stores.Any()) - { - TempData[WellKnownTempData.ErrorMessage] = "You need to be owner of at least one store before pairing"; - return RedirectToAction(nameof(UIHomeController.Index), "UIHome"); - } - return View(model); - } - - [HttpPost("/api-tokens")] - [AllowAnonymous] - public Task CreateToken2(CreateTokenViewModel model) - { - return CreateToken(model.StoreId, model); - } - - [HttpPost("{storeId}/tokens/apikey")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task GenerateAPIKey(string storeId, string command = "") - { - var store = HttpContext.GetStoreData(); - if (store == null) - return NotFound(); - if (command == "revoke") - { - await _TokenRepository.RevokeLegacyAPIKeys(CurrentStore.Id); - TempData[WellKnownTempData.SuccessMessage] = "API Key revoked"; - } - else - { - await _TokenRepository.GenerateLegacyAPIKey(CurrentStore.Id); - TempData[WellKnownTempData.SuccessMessage] = "API Key re-generated"; - } - - return RedirectToAction(nameof(ListTokens), new - { - storeId - }); - } - - [HttpGet("/api-access-request")] - [AllowAnonymous] - public async Task RequestPairing(string pairingCode, string? selectedStore = null) - { - var userId = GetUserId(); - if (userId == null) - return Challenge(AuthenticationSchemes.Cookie); - - if (pairingCode == null) - return NotFound(); - - if (selectedStore != null) - { - var store = await _Repo.FindStore(selectedStore, userId); - if (store == null) - return NotFound(); - HttpContext.SetStoreData(store); - } - - var pairing = await _TokenRepository.GetPairingAsync(pairingCode); - if (pairing == null) - { - TempData[WellKnownTempData.ErrorMessage] = "Unknown pairing code"; - return RedirectToAction(nameof(UIHomeController.Index), "UIHome"); - } - - var stores = (await _Repo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray(); - return View(new PairingModel - { - Id = pairing.Id, - Label = pairing.Label, - SIN = pairing.SIN ?? "Server-Initiated Pairing", - StoreId = selectedStore ?? stores.FirstOrDefault()?.Id, - Stores = stores.Select(s => new PairingModel.StoreViewModel - { - Id = s.Id, - Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName - }).ToArray() - }); - } - - [HttpPost("/api-access-request")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task Pair(string pairingCode, string storeId) - { - if (pairingCode == null) - return NotFound(); - var store = CurrentStore; - var pairing = await _TokenRepository.GetPairingAsync(pairingCode); - if (store == null || pairing == null) - return NotFound(); - - var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id); - if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial) - { - var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods(); - StoreNotConfigured = !store.GetPaymentMethodConfigs(_handlers) - .Where(p => !excludeFilter.Match(p.Key)) - .Any(); - TempData[WellKnownTempData.SuccessMessage] = "Pairing is successful"; - if (pairingResult == PairingResult.Partial) - TempData[WellKnownTempData.SuccessMessage] = "Server initiated pairing code: " + pairingCode; - return RedirectToAction(nameof(ListTokens), new - { - storeId = store.Id, - pairingCode = pairingCode - }); - } - else - { - TempData[WellKnownTempData.ErrorMessage] = $"Pairing failed ({pairingResult})"; - return RedirectToAction(nameof(ListTokens), new - { - storeId = store.Id - }); - } - } - private string? GetUserId() { if (User.Identity?.AuthenticationType != AuthenticationSchemes.Cookie) From df4d370524861fbfcd8b8701a8b73902548408db Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Thu, 4 Apr 2024 10:44:08 +0200 Subject: [PATCH 2/4] Rename and clean up properties --- .../UIStoresController.Dashboard.cs | 6 +- .../Controllers/UIStoresController.Email.cs | 6 +- .../UIStoresController.Integrations.cs | 30 +++---- .../UIStoresController.LightningLike.cs | 16 ++-- .../Controllers/UIStoresController.Onchain.cs | 36 ++++---- .../Controllers/UIStoresController.Rates.cs | 18 ++-- .../Controllers/UIStoresController.Roles.cs | 4 +- .../UIStoresController.Settings.cs | 18 ++-- .../Controllers/UIStoresController.Tokens.cs | 40 ++++----- .../Controllers/UIStoresController.Users.cs | 24 +++--- .../Controllers/UIStoresController.cs | 83 ++++++++----------- 11 files changed, 135 insertions(+), 146 deletions(-) diff --git a/BTCPayServer/Controllers/UIStoresController.Dashboard.cs b/BTCPayServer/Controllers/UIStoresController.Dashboard.cs index 91ff69b7f..3199c62fe 100644 --- a/BTCPayServer/Controllers/UIStoresController.Dashboard.cs +++ b/BTCPayServer/Controllers/UIStoresController.Dashboard.cs @@ -35,16 +35,16 @@ namespace BTCPayServer.Controllers var walletEnabled = derivationSchemes.Any(scheme => !string.IsNullOrEmpty(scheme.Value) && scheme.Enabled); var lightningEnabled = lightningNodes.Any(ln => !string.IsNullOrEmpty(ln.Address) && ln.Enabled); - var cryptoCode = _NetworkProvider.DefaultNetwork.CryptoCode; + var cryptoCode = _networkProvider.DefaultNetwork.CryptoCode; var vm = new StoreDashboardViewModel { WalletEnabled = walletEnabled, LightningEnabled = lightningEnabled, - LightningSupported = _NetworkProvider.GetNetwork(cryptoCode)?.SupportLightning is true, + LightningSupported = _networkProvider.GetNetwork(cryptoCode)?.SupportLightning is true, StoreId = CurrentStore.Id, StoreName = CurrentStore.StoreName, CryptoCode = cryptoCode, - Network = _NetworkProvider.DefaultNetwork, + Network = _networkProvider.DefaultNetwork, IsSetUp = walletEnabled || lightningEnabled }; diff --git a/BTCPayServer/Controllers/UIStoresController.Email.cs b/BTCPayServer/Controllers/UIStoresController.Email.cs index 170eefec8..7e02bf59d 100644 --- a/BTCPayServer/Controllers/UIStoresController.Email.cs +++ b/BTCPayServer/Controllers/UIStoresController.Email.cs @@ -100,7 +100,7 @@ namespace BTCPayServer.Controllers blob.EmailRules = vm.Rules; if (store.SetStoreBlob(blob)) { - await _Repo.UpdateStore(store); + await _storeRepo.UpdateStore(store); message += "Store email rules saved. "; } @@ -237,7 +237,7 @@ namespace BTCPayServer.Controllers var storeBlob = store.GetStoreBlob(); storeBlob.EmailSettings.Password = null; store.SetStoreBlob(storeBlob); - await _Repo.UpdateStore(store); + await _storeRepo.UpdateStore(store); TempData[WellKnownTempData.SuccessMessage] = "Email server password reset"; } if (useCustomSMTP) @@ -255,7 +255,7 @@ namespace BTCPayServer.Controllers } storeBlob.EmailSettings = model.Settings; store.SetStoreBlob(storeBlob); - await _Repo.UpdateStore(store); + await _storeRepo.UpdateStore(store); TempData[WellKnownTempData.SuccessMessage] = "Email settings modified"; } return RedirectToAction(nameof(StoreEmailSettings), new { storeId }); diff --git a/BTCPayServer/Controllers/UIStoresController.Integrations.cs b/BTCPayServer/Controllers/UIStoresController.Integrations.cs index d93e4030a..79da255ac 100644 --- a/BTCPayServer/Controllers/UIStoresController.Integrations.cs +++ b/BTCPayServer/Controllers/UIStoresController.Integrations.cs @@ -19,13 +19,13 @@ namespace BTCPayServer.Controllers { private async Task LastDeliveryForWebhook(string webhookId) { - return (await _Repo.GetWebhookDeliveries(CurrentStore.Id, webhookId, 1)).ToList().FirstOrDefault(); + return (await _storeRepo.GetWebhookDeliveries(CurrentStore.Id, webhookId, 1)).ToList().FirstOrDefault(); } [HttpGet("{storeId}/webhooks")] public async Task Webhooks() { - var webhooks = await _Repo.GetWebhooks(CurrentStore.Id); + var webhooks = await _storeRepo.GetWebhooks(CurrentStore.Id); return View(nameof(Webhooks), new WebhooksViewModel() { Webhooks = webhooks.Select(async w => @@ -63,7 +63,7 @@ namespace BTCPayServer.Controllers [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] public async Task DeleteWebhook(string webhookId) { - var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId); + var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId); if (webhook is null) return NotFound(); @@ -74,11 +74,11 @@ namespace BTCPayServer.Controllers [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] public async Task DeleteWebhookPost(string webhookId) { - var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId); + var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId); if (webhook is null) return NotFound(); - await _Repo.DeleteWebhook(CurrentStore.Id, webhookId); + await _storeRepo.DeleteWebhook(CurrentStore.Id, webhookId); TempData[WellKnownTempData.SuccessMessage] = "Webhook successfully deleted"; return RedirectToAction(nameof(Webhooks), new { storeId = CurrentStore.Id }); } @@ -90,7 +90,7 @@ namespace BTCPayServer.Controllers if (!ModelState.IsValid) return View(nameof(ModifyWebhook), viewModel); - await _Repo.CreateWebhook(CurrentStore.Id, viewModel.CreateBlob()); + await _storeRepo.CreateWebhook(CurrentStore.Id, viewModel.CreateBlob()); TempData[WellKnownTempData.SuccessMessage] = "The webhook has been created"; return RedirectToAction(nameof(Webhooks), new { storeId }); } @@ -99,12 +99,12 @@ namespace BTCPayServer.Controllers [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] public async Task ModifyWebhook(string webhookId) { - var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId); + var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId); if (webhook is null) return NotFound(); var blob = webhook.GetBlob(); - var deliveries = await _Repo.GetWebhookDeliveries(CurrentStore.Id, webhookId, 20); + var deliveries = await _storeRepo.GetWebhookDeliveries(CurrentStore.Id, webhookId, 20); return View(nameof(ModifyWebhook), new EditWebhookViewModel(blob) { Deliveries = deliveries @@ -116,13 +116,13 @@ namespace BTCPayServer.Controllers [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] public async Task ModifyWebhook(string webhookId, EditWebhookViewModel viewModel) { - var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId); + var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId); if (webhook is null) return NotFound(); if (!ModelState.IsValid) return View(nameof(ModifyWebhook), viewModel); - await _Repo.UpdateWebhook(CurrentStore.Id, webhookId, viewModel.CreateBlob()); + await _storeRepo.UpdateWebhook(CurrentStore.Id, webhookId, viewModel.CreateBlob()); TempData[WellKnownTempData.SuccessMessage] = "The webhook has been updated"; return RedirectToAction(nameof(Webhooks), new { storeId = CurrentStore.Id }); } @@ -131,7 +131,7 @@ namespace BTCPayServer.Controllers [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] public async Task TestWebhook(string webhookId) { - var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId); + var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId); if (webhook is null) return NotFound(); @@ -142,7 +142,7 @@ namespace BTCPayServer.Controllers [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] public async Task TestWebhook(string webhookId, TestWebhookViewModel viewModel, CancellationToken cancellationToken) { - var result = await WebhookNotificationManager.TestWebhook(CurrentStore.Id, webhookId, viewModel.Type, cancellationToken); + var result = await _webhookNotificationManager.TestWebhook(CurrentStore.Id, webhookId, viewModel.Type, cancellationToken); if (result.Success) { @@ -160,11 +160,11 @@ namespace BTCPayServer.Controllers [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] public async Task RedeliverWebhook(string webhookId, string deliveryId) { - var delivery = await _Repo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId); + var delivery = await _storeRepo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId); if (delivery is null) return NotFound(); - var newDeliveryId = await WebhookNotificationManager.Redeliver(deliveryId); + var newDeliveryId = await _webhookNotificationManager.Redeliver(deliveryId); if (newDeliveryId is null) return NotFound(); @@ -181,7 +181,7 @@ namespace BTCPayServer.Controllers [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] public async Task WebhookDelivery(string webhookId, string deliveryId) { - var delivery = await _Repo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId); + var delivery = await _storeRepo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId); if (delivery is null) return NotFound(); diff --git a/BTCPayServer/Controllers/UIStoresController.LightningLike.cs b/BTCPayServer/Controllers/UIStoresController.LightningLike.cs index 3fcf86e41..b058867ae 100644 --- a/BTCPayServer/Controllers/UIStoresController.LightningLike.cs +++ b/BTCPayServer/Controllers/UIStoresController.LightningLike.cs @@ -50,7 +50,7 @@ namespace BTCPayServer.Controllers }; try { - model.Link = await service.GetLink(Request.GetAbsoluteUriNoPathBase(), _BtcpayServerOptions.NetworkType); + model.Link = await service.GetLink(Request.GetAbsoluteUriNoPathBase(), _btcpayServerOptions.NetworkType); } catch (Exception exception) { @@ -108,7 +108,7 @@ namespace BTCPayServer.Controllers if (store == null) return NotFound(); - var network = _ExplorerProvider.GetNetwork(vm.CryptoCode); + var network = _explorerProvider.GetNetwork(vm.CryptoCode); var oldConf = _handlers.GetLightningConfig(store, network); vm.CanUseInternalNode = CanUseInternalLightning(vm.CryptoCode); @@ -159,7 +159,7 @@ namespace BTCPayServer.Controllers LUD12Enabled = false }); - await _Repo.UpdateStore(store); + await _storeRepo.UpdateStore(store); TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning node updated."; return RedirectToAction(nameof(LightningSettings), new { storeId, cryptoCode }); @@ -246,7 +246,7 @@ namespace BTCPayServer.Controllers return View(vm); } - var network = _ExplorerProvider.GetNetwork(vm.CryptoCode); + var network = _explorerProvider.GetNetwork(vm.CryptoCode); var needUpdate = false; var blob = store.GetStoreBlob(); blob.LightningDescriptionTemplate = vm.LightningDescriptionTemplate ?? string.Empty; @@ -277,7 +277,7 @@ namespace BTCPayServer.Controllers if (needUpdate) { - await _Repo.UpdateStore(store); + await _storeRepo.UpdateStore(store); TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning settings successfully updated."; } @@ -296,7 +296,7 @@ namespace BTCPayServer.Controllers if (cryptoCode == null) return NotFound(); - var network = _ExplorerProvider.GetNetwork(cryptoCode); + var network = _explorerProvider.GetNetwork(cryptoCode); var lightning = GetConfig(PaymentTypes.LN.GetPaymentMethodId(cryptoCode), store); if (lightning == null) return NotFound(); @@ -309,7 +309,7 @@ namespace BTCPayServer.Controllers storeBlob.SetExcluded(PaymentTypes.LNURL.GetPaymentMethodId(network.CryptoCode), true); } store.SetStoreBlob(storeBlob); - await _Repo.UpdateStore(store); + await _storeRepo.UpdateStore(store); TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning payments are now {(enabled ? "enabled" : "disabled")} for this store."; return RedirectToAction(nameof(LightningSettings), new { storeId, cryptoCode }); @@ -317,7 +317,7 @@ namespace BTCPayServer.Controllers private bool CanUseInternalLightning(string cryptoCode) { - return LightningNetworkOptions.InternalLightningByCryptoCode.ContainsKey(cryptoCode.ToUpperInvariant()) && (User.IsInRole(Roles.ServerAdmin) || _policiesSettings.AllowLightningInternalNodeForAll); + return _lightningNetworkOptions.InternalLightningByCryptoCode.ContainsKey(cryptoCode.ToUpperInvariant()) && (User.IsInRole(Roles.ServerAdmin) || _policiesSettings.AllowLightningInternalNodeForAll); } private void SetExistingValues(StoreData store, LightningNodeViewModel vm) diff --git a/BTCPayServer/Controllers/UIStoresController.Onchain.cs b/BTCPayServer/Controllers/UIStoresController.Onchain.cs index eda13c622..70ff48d33 100644 --- a/BTCPayServer/Controllers/UIStoresController.Onchain.cs +++ b/BTCPayServer/Controllers/UIStoresController.Onchain.cs @@ -88,7 +88,7 @@ namespace BTCPayServer.Controllers DerivationSchemeSettings strategy = null; PaymentMethodId paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode); BitcoinLikePaymentHandler handler = (BitcoinLikePaymentHandler)_handlers[paymentMethodId]; - var wallet = _WalletProvider.GetWallet(network); + var wallet = _walletProvider.GetWallet(network); if (wallet == null) { return NotFound(); @@ -185,8 +185,8 @@ namespace BTCPayServer.Controllers ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid derivation scheme"); return View(vm.ViewName, vm); } - await _Repo.UpdateStore(store); - _EventAggregator.Publish(new WalletChangedEvent { WalletId = new WalletId(vm.StoreId, vm.CryptoCode) }); + await _storeRepo.UpdateStore(store); + _eventAggregator.Publish(new WalletChangedEvent { WalletId = new WalletId(vm.StoreId, vm.CryptoCode) }); TempData[WellKnownTempData.SuccessMessage] = $"Wallet settings for {network.CryptoCode} have been updated."; @@ -198,11 +198,11 @@ namespace BTCPayServer.Controllers private string ProtectString(string str) { - return Convert.ToBase64String(DataProtector.Protect(Encoding.UTF8.GetBytes(str))); + return Convert.ToBase64String(_dataProtector.Protect(Encoding.UTF8.GetBytes(str))); } private string UnprotectString(string str) { - return Encoding.UTF8.GetString(DataProtector.Unprotect(Convert.FromBase64String(str))); + return Encoding.UTF8.GetString(_dataProtector.Unprotect(Convert.FromBase64String(str))); } [HttpGet("{storeId}/onchain/{cryptoCode}/generate/{method?}")] @@ -264,7 +264,7 @@ namespace BTCPayServer.Controllers return NotFound(); } var handler = _handlers.GetBitcoinHandler(cryptoCode); - var client = _ExplorerProvider.GetExplorerClient(cryptoCode); + var client = _explorerProvider.GetExplorerClient(cryptoCode); var isImport = method == WalletSetupMethod.Seed; var vm = new WalletSetupViewModel { @@ -351,7 +351,7 @@ namespace BTCPayServer.Controllers IsStored = request.SavePrivateKeys, ReturnUrl = Url.Action(nameof(GenerateWalletConfirm), new { storeId, cryptoCode }) }; - if (_BTCPayEnv.IsDeveloping) + if (_btcPayEnv.IsDeveloping) { GenerateWalletResponse = response; } @@ -403,7 +403,7 @@ namespace BTCPayServer.Controllers var storeBlob = store.GetStoreBlob(); var excludeFilters = storeBlob.GetExcludedPaymentMethods(); (bool canUseHotWallet, bool rpcImport) = await CanUseHotWallet(); - var client = _ExplorerProvider.GetExplorerClient(network); + var client = _explorerProvider.GetExplorerClient(network); var handler = _handlers.GetBitcoinHandler(cryptoCode); var vm = new WalletSettingsViewModel @@ -546,14 +546,14 @@ namespace BTCPayServer.Controllers { store.SetPaymentMethodConfig(handler, derivation); - await _Repo.UpdateStore(store); + await _storeRepo.UpdateStore(store); if (string.IsNullOrEmpty(errorMessage)) { var successMessage = "Wallet settings successfully updated."; if (enabledChanged) { - _EventAggregator.Publish(new WalletChangedEvent { WalletId = new WalletId(vm.StoreId, vm.CryptoCode) }); + _eventAggregator.Publish(new WalletChangedEvent { WalletId = new WalletId(vm.StoreId, vm.CryptoCode) }); successMessage += $" {vm.CryptoCode} on-chain payments are now {(vm.Enabled ? "enabled" : "disabled")} for this store."; } @@ -599,7 +599,7 @@ namespace BTCPayServer.Controllers if (needUpdate) { - await _Repo.UpdateStore(store); + await _storeRepo.UpdateStore(store); TempData[WellKnownTempData.SuccessMessage] = "Payment settings successfully updated"; @@ -644,7 +644,7 @@ namespace BTCPayServer.Controllers return NotFound(); } - var client = _ExplorerProvider.GetExplorerClient(network); + var client = _explorerProvider.GetExplorerClient(network); if (await GetSeed(client, derivation) != null) { var mnemonic = await client.GetMetadataAsync(derivation.AccountDerivation, @@ -746,8 +746,8 @@ namespace BTCPayServer.Controllers store.SetPaymentMethodConfig(PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode), null); - await _Repo.UpdateStore(store); - _EventAggregator.Publish(new WalletChangedEvent { WalletId = new WalletId(storeId, cryptoCode) }); + await _storeRepo.UpdateStore(store); + _eventAggregator.Publish(new WalletChangedEvent { WalletId = new WalletId(storeId, cryptoCode) }); TempData[WellKnownTempData.SuccessMessage] = $"On-Chain payment for {network.CryptoCode} has been removed."; @@ -785,7 +785,7 @@ namespace BTCPayServer.Controllers private ActionResult IsAvailable(string cryptoCode, out StoreData store, out BTCPayNetwork network) { store = HttpContext.GetStoreData(); - network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode); + network = cryptoCode == null ? null : _explorerProvider.GetNetwork(cryptoCode); return store == null || network == null ? NotFound() : null; } @@ -820,9 +820,9 @@ namespace BTCPayServer.Controllers ? "" : " or imported it into an external wallet. If you no longer have access to your private key (recovery seed), immediately replace the wallet"; return - $"

Please note that this is a {Html.Encode(walletType)} wallet!

" + - $"

Do not proceed if you have not backed up the wallet{Html.Encode(additionalText)}.

" + - $"

This action will erase the current wallet data from the server. {Html.Encode(info)}

"; + $"

Please note that this is a {_html.Encode(walletType)} wallet!

" + + $"

Do not proceed if you have not backed up the wallet{_html.Encode(additionalText)}.

" + + $"

This action will erase the current wallet data from the server. {_html.Encode(info)}

"; } private string WalletReplaceWarning(bool isHotWallet) diff --git a/BTCPayServer/Controllers/UIStoresController.Rates.cs b/BTCPayServer/Controllers/UIStoresController.Rates.cs index d88cc18a1..30f1e323d 100644 --- a/BTCPayServer/Controllers/UIStoresController.Rates.cs +++ b/BTCPayServer/Controllers/UIStoresController.Rates.cs @@ -27,8 +27,8 @@ namespace BTCPayServer.Controllers vm.SetExchangeRates(exchanges, storeBlob.PreferredExchange ?? storeBlob.GetRecommendedExchange()); vm.Spread = (double)(storeBlob.Spread * 100m); vm.StoreId = CurrentStore.Id; - vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString(); - vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString(); + vm.Script = storeBlob.GetRateRules(_networkProvider).ToString(); + vm.DefaultScript = storeBlob.GetDefaultRateRules(_networkProvider).ToString(); vm.AvailableExchanges = exchanges; vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString(); vm.ShowScripting = storeBlob.RateScripting; @@ -71,7 +71,7 @@ namespace BTCPayServer.Controllers model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant(); var blob = CurrentStore.GetStoreBlob(); - model.DefaultScript = blob.GetDefaultRateRules(_NetworkProvider).ToString(); + model.DefaultScript = blob.GetDefaultRateRules(_networkProvider).ToString(); model.AvailableExchanges = exchanges; blob.PreferredExchange = model.PreferredExchange; @@ -102,7 +102,7 @@ namespace BTCPayServer.Controllers model.Script = blob.RateScript; } } - rules = blob.GetRateRules(_NetworkProvider); + rules = blob.GetRateRules(_networkProvider); if (command == "Test") { @@ -124,7 +124,7 @@ namespace BTCPayServer.Controllers pairs.Add(currencyPair); } - var fetchs = _RateFactory.FetchRates(pairs.ToHashSet(), rules, cancellationToken); + var fetchs = _rateFactory.FetchRates(pairs.ToHashSet(), rules, cancellationToken); var testResults = new List(); foreach (var fetch in fetchs) { @@ -144,7 +144,7 @@ namespace BTCPayServer.Controllers { if (CurrentStore.SetStoreBlob(blob)) { - await _Repo.UpdateStore(CurrentStore); + await _storeRepo.UpdateStore(CurrentStore); TempData[WellKnownTempData.SuccessMessage] = "Rate settings updated"; } return RedirectToAction(nameof(Rates), new @@ -175,16 +175,16 @@ namespace BTCPayServer.Controllers { var blob = CurrentStore.GetStoreBlob(); blob.RateScripting = scripting; - blob.RateScript = blob.GetDefaultRateRules(_NetworkProvider).ToString(); + blob.RateScript = blob.GetDefaultRateRules(_networkProvider).ToString(); CurrentStore.SetStoreBlob(blob); - await _Repo.UpdateStore(CurrentStore); + await _storeRepo.UpdateStore(CurrentStore); TempData[WellKnownTempData.SuccessMessage] = "Rate rules scripting " + (scripting ? "activated" : "deactivated"); return RedirectToAction(nameof(Rates), new { storeId = CurrentStore.Id }); } private IEnumerable GetSupportedExchanges() { - return _RateFactory.RateProviderFactory.AvailableRateProviders + return _rateFactory.RateProviderFactory.AvailableRateProviders .OrderBy(s => s.DisplayName, StringComparer.OrdinalIgnoreCase); } } diff --git a/BTCPayServer/Controllers/UIStoresController.Roles.cs b/BTCPayServer/Controllers/UIStoresController.Roles.cs index 35169bb52..2e249daa5 100644 --- a/BTCPayServer/Controllers/UIStoresController.Roles.cs +++ b/BTCPayServer/Controllers/UIStoresController.Roles.cs @@ -129,9 +129,9 @@ namespace BTCPayServer.Controllers return View("Confirm", roleData.IsUsed is true ? new ConfirmModel("Delete role", - $"Unable to proceed: The role {Html.Encode(roleData.Role)} is currently assigned to one or more users, it cannot be removed.") + $"Unable to proceed: The role {_html.Encode(roleData.Role)} is currently assigned to one or more users, it cannot be removed.") : new ConfirmModel("Delete role", - $"The role {Html.Encode(roleData.Role)} will be permanently deleted. Are you sure?", + $"The role {_html.Encode(roleData.Role)} will be permanently deleted. Are you sure?", "Delete")); } diff --git a/BTCPayServer/Controllers/UIStoresController.Settings.cs b/BTCPayServer/Controllers/UIStoresController.Settings.cs index db3013557..7e194ec9d 100644 --- a/BTCPayServer/Controllers/UIStoresController.Settings.cs +++ b/BTCPayServer/Controllers/UIStoresController.Settings.cs @@ -41,7 +41,7 @@ namespace BTCPayServer.Controllers DefaultCurrency = storeBlob.DefaultCurrency, BOLT11Expiration = (long)storeBlob.RefundBOLT11Expiration.TotalDays, Archived = store.Archived, - CanDelete = _Repo.CanDeleteStores() + CanDelete = _storeRepo.CanDeleteStores() }; return View(vm); @@ -177,7 +177,7 @@ namespace BTCPayServer.Controllers if (needUpdate) { - await _Repo.UpdateStore(CurrentStore); + await _storeRepo.UpdateStore(CurrentStore); TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated"; } @@ -193,7 +193,7 @@ namespace BTCPayServer.Controllers public async Task ToggleArchive(string storeId) { CurrentStore.Archived = !CurrentStore.Archived; - await _Repo.UpdateStore(CurrentStore); + await _storeRepo.UpdateStore(CurrentStore); TempData[WellKnownTempData.SuccessMessage] = CurrentStore.Archived ? "The store has been archived and will no longer appear in the stores list by default." @@ -216,7 +216,7 @@ namespace BTCPayServer.Controllers [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] public async Task DeleteStorePost(string storeId) { - await _Repo.DeleteStore(CurrentStore.Id); + await _storeRepo.DeleteStore(CurrentStore.Id); TempData[WellKnownTempData.SuccessMessage] = "Store successfully deleted."; return RedirectToAction(nameof(UIHomeController.Index), "UIHome"); } @@ -264,7 +264,7 @@ namespace BTCPayServer.Controllers vm.DisplayExpirationTimer = (int)storeBlob.DisplayExpirationTimer.TotalMinutes; vm.ReceiptOptions = CheckoutAppearanceViewModel.ReceiptOptionsViewModel.Create(storeBlob.ReceiptOptions); vm.AutoDetectLanguage = storeBlob.AutoDetectLanguage; - vm.SetLanguages(_LangService, storeBlob.DefaultLang); + vm.SetLanguages(_langService, storeBlob.DefaultLang); return View(vm); } @@ -282,7 +282,7 @@ namespace BTCPayServer.Controllers CurrentStore.SetDefaultPaymentId(defaultPaymentMethodId); } SetCryptoCurrencies(model, CurrentStore); - model.SetLanguages(_LangService, model.DefaultLang); + model.SetLanguages(_langService, model.DefaultLang); model.PaymentMethodCriteria ??= new List(); for (var index = 0; index < model.PaymentMethodCriteria.Count; index++) { @@ -409,7 +409,7 @@ namespace BTCPayServer.Controllers } if (needUpdate) { - await _Repo.UpdateStore(CurrentStore); + await _storeRepo.UpdateStore(CurrentStore); TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated"; } @@ -435,8 +435,8 @@ namespace BTCPayServer.Controllers var defaultChoice = defaultPaymentId is not null ? defaultPaymentId.FindNearest(enabled) : null; if (defaultChoice is null) { - defaultChoice = enabled.FirstOrDefault(e => e == PaymentTypes.CHAIN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode)) ?? - enabled.FirstOrDefault(e => e == PaymentTypes.LN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode)) ?? + defaultChoice = enabled.FirstOrDefault(e => e == PaymentTypes.CHAIN.GetPaymentMethodId(_networkProvider.DefaultNetwork.CryptoCode)) ?? + enabled.FirstOrDefault(e => e == PaymentTypes.LN.GetPaymentMethodId(_networkProvider.DefaultNetwork.CryptoCode)) ?? enabled.FirstOrDefault(); } var choices = GetEnabledPaymentMethodChoices(storeData); diff --git a/BTCPayServer/Controllers/UIStoresController.Tokens.cs b/BTCPayServer/Controllers/UIStoresController.Tokens.cs index 8d677f993..8093c7788 100644 --- a/BTCPayServer/Controllers/UIStoresController.Tokens.cs +++ b/BTCPayServer/Controllers/UIStoresController.Tokens.cs @@ -23,7 +23,7 @@ namespace BTCPayServer.Controllers public async Task ListTokens() { var model = new TokensViewModel(); - var tokens = await _TokenRepository.GetTokensByStoreIdAsync(CurrentStore.Id); + var tokens = await _tokenRepository.GetTokensByStoreIdAsync(CurrentStore.Id); model.StoreNotConfigured = StoreNotConfigured; model.Tokens = tokens.Select(t => new TokenViewModel() { @@ -32,7 +32,7 @@ namespace BTCPayServer.Controllers Id = t.Value }).ToArray(); - model.ApiKey = (await _TokenRepository.GetLegacyAPIKeys(CurrentStore.Id)).FirstOrDefault(); + model.ApiKey = (await _tokenRepository.GetLegacyAPIKeys(CurrentStore.Id)).FirstOrDefault(); if (model.ApiKey == null) model.EncodedApiKey = "*API Key*"; else @@ -44,20 +44,20 @@ namespace BTCPayServer.Controllers [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] public async Task RevokeToken(string tokenId) { - var token = await _TokenRepository.GetToken(tokenId); + var token = await _tokenRepository.GetToken(tokenId); if (token == null || token.StoreId != CurrentStore.Id) return NotFound(); - return View("Confirm", new ConfirmModel("Revoke the token", $"The access token with the label {Html.Encode(token.Label)} will be revoked. Do you wish to continue?", "Revoke")); + return View("Confirm", new ConfirmModel("Revoke the token", $"The access token with the label {_html.Encode(token.Label)} will be revoked. Do you wish to continue?", "Revoke")); } [HttpPost("{storeId}/tokens/{tokenId}/revoke")] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] public async Task RevokeTokenConfirm(string tokenId) { - var token = await _TokenRepository.GetToken(tokenId); + var token = await _tokenRepository.GetToken(tokenId); if (token == null || token.StoreId != CurrentStore.Id || - !await _TokenRepository.DeleteToken(tokenId)) + !await _tokenRepository.DeleteToken(tokenId)) TempData[WellKnownTempData.ErrorMessage] = "Failure to revoke this token."; else TempData[WellKnownTempData.SuccessMessage] = "Token revoked"; @@ -68,7 +68,7 @@ namespace BTCPayServer.Controllers [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] public async Task ShowToken(string tokenId) { - var token = await _TokenRepository.GetToken(tokenId); + var token = await _tokenRepository.GetToken(tokenId); if (token == null || token.StoreId != CurrentStore.Id) return NotFound(); return View(token); @@ -101,7 +101,7 @@ namespace BTCPayServer.Controllers var store = model.StoreId switch { null => CurrentStore, - _ => await _Repo.FindStore(storeId, userId) + _ => await _storeRepo.FindStore(storeId, userId) }; if (store == null) return Challenge(AuthenticationSchemes.Cookie); @@ -114,18 +114,18 @@ namespace BTCPayServer.Controllers string? pairingCode = null; if (model.PublicKey == null) { - tokenRequest.PairingCode = await _TokenRepository.CreatePairingCodeAsync(); - await _TokenRepository.UpdatePairingCode(new PairingCodeEntity() + tokenRequest.PairingCode = await _tokenRepository.CreatePairingCodeAsync(); + await _tokenRepository.UpdatePairingCode(new PairingCodeEntity() { Id = tokenRequest.PairingCode, Label = model.Label, }); - await _TokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, store.Id); + await _tokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, store.Id); pairingCode = tokenRequest.PairingCode; } else { - pairingCode = (await _TokenController.Tokens(tokenRequest)).Data[0].PairingCode; + pairingCode = (await _tokenController.Tokens(tokenRequest)).Data[0].PairingCode; } GeneratedPairingCode = pairingCode; @@ -147,7 +147,7 @@ namespace BTCPayServer.Controllers ViewBag.HidePublicKey = true; ViewBag.ShowStores = true; ViewBag.ShowMenu = false; - var stores = (await _Repo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray(); + var stores = (await _storeRepo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray(); model.Stores = new SelectList(stores, nameof(CurrentStore.Id), nameof(CurrentStore.StoreName)); if (!model.Stores.Any()) @@ -174,12 +174,12 @@ namespace BTCPayServer.Controllers return NotFound(); if (command == "revoke") { - await _TokenRepository.RevokeLegacyAPIKeys(CurrentStore.Id); + await _tokenRepository.RevokeLegacyAPIKeys(CurrentStore.Id); TempData[WellKnownTempData.SuccessMessage] = "API Key revoked"; } else { - await _TokenRepository.GenerateLegacyAPIKey(CurrentStore.Id); + await _tokenRepository.GenerateLegacyAPIKey(CurrentStore.Id); TempData[WellKnownTempData.SuccessMessage] = "API Key re-generated"; } @@ -202,20 +202,20 @@ namespace BTCPayServer.Controllers if (selectedStore != null) { - var store = await _Repo.FindStore(selectedStore, userId); + var store = await _storeRepo.FindStore(selectedStore, userId); if (store == null) return NotFound(); HttpContext.SetStoreData(store); } - var pairing = await _TokenRepository.GetPairingAsync(pairingCode); + var pairing = await _tokenRepository.GetPairingAsync(pairingCode); if (pairing == null) { TempData[WellKnownTempData.ErrorMessage] = "Unknown pairing code"; return RedirectToAction(nameof(UIHomeController.Index), "UIHome"); } - var stores = (await _Repo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray(); + var stores = (await _storeRepo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray(); return View(new PairingModel { Id = pairing.Id, @@ -237,11 +237,11 @@ namespace BTCPayServer.Controllers if (pairingCode == null) return NotFound(); var store = CurrentStore; - var pairing = await _TokenRepository.GetPairingAsync(pairingCode); + var pairing = await _tokenRepository.GetPairingAsync(pairingCode); if (store == null || pairing == null) return NotFound(); - var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id); + var pairingResult = await _tokenRepository.PairWithStoreAsync(pairingCode, store.Id); if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial) { var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods(); diff --git a/BTCPayServer/Controllers/UIStoresController.Users.cs b/BTCPayServer/Controllers/UIStoresController.Users.cs index 467327e5f..dcf7a87e0 100644 --- a/BTCPayServer/Controllers/UIStoresController.Users.cs +++ b/BTCPayServer/Controllers/UIStoresController.Users.cs @@ -36,16 +36,16 @@ namespace BTCPayServer.Controllers return View(vm); } - var roles = await _Repo.GetStoreRoles(CurrentStore.Id); + var roles = await _storeRepo.GetStoreRoles(CurrentStore.Id); if (roles.All(role => role.Id != vm.Role)) { ModelState.AddModelError(nameof(vm.Role), "Invalid role"); return View(vm); } - var user = await _UserManager.FindByEmailAsync(vm.Email); + var user = await _userManager.FindByEmailAsync(vm.Email); var isExistingUser = user is not null; - var isExistingStoreUser = isExistingUser && await _Repo.GetStoreUser(storeId, user!.Id) is not null; + var isExistingStoreUser = isExistingUser && await _storeRepo.GetStoreUser(storeId, user!.Id) is not null; var successInfo = string.Empty; if (user == null) { @@ -58,11 +58,11 @@ namespace BTCPayServer.Controllers Created = DateTimeOffset.UtcNow }; - var result = await _UserManager.CreateAsync(user); + var result = await _userManager.CreateAsync(user); if (result.Succeeded) { var tcs = new TaskCompletionSource(); - var currentUser = await _UserManager.GetUserAsync(HttpContext.User); + var currentUser = await _userManager.GetUserAsync(HttpContext.User); _eventAggregator.Publish(new UserRegisteredEvent { @@ -87,11 +87,11 @@ namespace BTCPayServer.Controllers } } - var roleId = await _Repo.ResolveStoreRoleId(storeId, vm.Role); + var roleId = await _storeRepo.ResolveStoreRoleId(storeId, vm.Role); var action = isExistingUser ? isExistingStoreUser ? "updated" : "added" : "invited"; - if (await _Repo.AddOrUpdateStoreUser(CurrentStore.Id, user.Id, roleId)) + if (await _storeRepo.AddOrUpdateStoreUser(CurrentStore.Id, user.Id, roleId)) { TempData.SetStatusMessageModel(new StatusMessageModel { @@ -110,14 +110,14 @@ namespace BTCPayServer.Controllers [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] public async Task UpdateStoreUser(string storeId, string userId, StoreUsersViewModel.StoreUserViewModel vm) { - var roleId = await _Repo.ResolveStoreRoleId(storeId, vm.Role); - var storeUsers = await _Repo.GetStoreUsers(storeId); + var roleId = await _storeRepo.ResolveStoreRoleId(storeId, vm.Role); + var storeUsers = await _storeRepo.GetStoreUsers(storeId); var user = storeUsers.First(user => user.Id == userId); var isOwner = user.StoreRole.Id == StoreRoleId.Owner.Id; var isLastOwner = isOwner && storeUsers.Count(u => u.StoreRole.Id == StoreRoleId.Owner.Id) == 1; if (isLastOwner && roleId != StoreRoleId.Owner) TempData[WellKnownTempData.ErrorMessage] = $"User {user.Email} is the last owner. Their role cannot be changed."; - else if (await _Repo.AddOrUpdateStoreUser(storeId, userId, roleId)) + else if (await _storeRepo.AddOrUpdateStoreUser(storeId, userId, roleId)) TempData[WellKnownTempData.SuccessMessage] = $"The role of {user.Email} has been changed to {vm.Role}."; return RedirectToAction(nameof(StoreUsers), new { storeId, userId }); } @@ -126,7 +126,7 @@ namespace BTCPayServer.Controllers [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] public async Task DeleteStoreUser(string storeId, string userId) { - if (await _Repo.RemoveStoreUser(storeId, userId)) + if (await _storeRepo.RemoveStoreUser(storeId, userId)) TempData[WellKnownTempData.SuccessMessage] = "User removed successfully."; else TempData[WellKnownTempData.ErrorMessage] = "Removing this user would result in the store having no owner."; @@ -135,7 +135,7 @@ namespace BTCPayServer.Controllers private async Task FillUsers(StoreUsersViewModel vm) { - var users = await _Repo.GetStoreUsers(CurrentStore.Id); + var users = await _storeRepo.GetStoreUsers(CurrentStore.Id); vm.StoreId = CurrentStore.Id; vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel() { diff --git a/BTCPayServer/Controllers/UIStoresController.cs b/BTCPayServer/Controllers/UIStoresController.cs index b49b4a6dc..4e98be73e 100644 --- a/BTCPayServer/Controllers/UIStoresController.cs +++ b/BTCPayServer/Controllers/UIStoresController.cs @@ -33,10 +33,9 @@ namespace BTCPayServer.Controllers public partial class UIStoresController : Controller { public UIStoresController( - IServiceProvider serviceProvider, BTCPayServerOptions btcpayServerOptions, BTCPayServerEnvironment btcpayEnv, - StoreRepository repo, + StoreRepository storeRepo, TokenRepository tokenRepo, UserManager userManager, BitpayAccessTokenController tokenController, @@ -55,88 +54,78 @@ namespace BTCPayServer.Controllers IOptions lightningNetworkOptions, IOptions externalServiceOptions, IHtmlHelper html, - LightningClientFactoryService lightningClientFactoryService, EmailSenderFactory emailSenderFactory, WalletFileParsers onChainWalletParsers, SettingsRepository settingsRepository, EventAggregator eventAggregator) { - _RateFactory = rateFactory; - _Repo = repo; - _TokenRepository = tokenRepo; - _UserManager = userManager; - _LangService = langService; - _TokenController = tokenController; - _WalletProvider = walletProvider; + _rateFactory = rateFactory; + _storeRepo = storeRepo; + _tokenRepository = tokenRepo; + _userManager = userManager; + _langService = langService; + _tokenController = tokenController; + _walletProvider = walletProvider; _handlers = paymentMethodHandlerDictionary; _policiesSettings = policiesSettings; _authorizationService = authorizationService; _appService = appService; _fileService = fileService; - DataProtector = dataProtector.CreateProtector("ConfigProtector"); - WebhookNotificationManager = webhookNotificationManager; - LightningNetworkOptions = lightningNetworkOptions.Value; - _EventAggregator = eventAggregator; - _NetworkProvider = networkProvider; - _ExplorerProvider = explorerProvider; - _ServiceProvider = serviceProvider; - _BtcpayServerOptions = btcpayServerOptions; - _BTCPayEnv = btcpayEnv; + _networkProvider = networkProvider; + _explorerProvider = explorerProvider; + _btcpayServerOptions = btcpayServerOptions; + _btcPayEnv = btcpayEnv; _externalServiceOptions = externalServiceOptions; - _lightningClientFactoryService = lightningClientFactoryService; _emailSenderFactory = emailSenderFactory; _onChainWalletParsers = onChainWalletParsers; _settingsRepository = settingsRepository; _eventAggregator = eventAggregator; - Html = html; + _html = html; + _dataProtector = dataProtector.CreateProtector("ConfigProtector"); + _webhookNotificationManager = webhookNotificationManager; + _lightningNetworkOptions = lightningNetworkOptions.Value; } - readonly BTCPayServerOptions _BtcpayServerOptions; - readonly BTCPayServerEnvironment _BTCPayEnv; - readonly IServiceProvider _ServiceProvider; - readonly BTCPayNetworkProvider _NetworkProvider; - readonly BTCPayWalletProvider _WalletProvider; - readonly BitpayAccessTokenController _TokenController; - readonly StoreRepository _Repo; - readonly TokenRepository _TokenRepository; - readonly UserManager _UserManager; - readonly RateFetcher _RateFactory; - readonly SettingsRepository _settingsRepository; - private readonly ExplorerClientProvider _ExplorerProvider; - private readonly LanguageService _LangService; + private readonly BTCPayServerOptions _btcpayServerOptions; + private readonly BTCPayServerEnvironment _btcPayEnv; + private readonly BTCPayNetworkProvider _networkProvider; + private readonly BTCPayWalletProvider _walletProvider; + private readonly BitpayAccessTokenController _tokenController; + private readonly StoreRepository _storeRepo; + private readonly TokenRepository _tokenRepository; + private readonly UserManager _userManager; + private readonly RateFetcher _rateFactory; + private readonly SettingsRepository _settingsRepository; + private readonly ExplorerClientProvider _explorerProvider; + private readonly LanguageService _langService; private readonly PaymentMethodHandlerDictionary _handlers; private readonly PoliciesSettings _policiesSettings; private readonly IAuthorizationService _authorizationService; private readonly AppService _appService; private readonly IFileService _fileService; - private readonly EventAggregator _EventAggregator; private readonly IOptions _externalServiceOptions; - private readonly LightningClientFactoryService _lightningClientFactoryService; private readonly EmailSenderFactory _emailSenderFactory; private readonly WalletFileParsers _onChainWalletParsers; private readonly EventAggregator _eventAggregator; + private readonly IHtmlHelper _html; + private readonly WebhookSender _webhookNotificationManager; + private readonly LightningNetworkOptions _lightningNetworkOptions; + private readonly IDataProtector _dataProtector; public string? GeneratedPairingCode { get; set; } - public WebhookSender WebhookNotificationManager { get; } - public IHtmlHelper Html { get; } - public LightningNetworkOptions LightningNetworkOptions { get; } - public IDataProtector DataProtector { get; } [TempData] - public bool StoreNotConfigured - { - get; set; - } + private bool StoreNotConfigured { get; set; } [AllowAnonymous] [HttpGet("{storeId}/index")] public async Task Index(string storeId) { - var userId = _UserManager.GetUserId(User); + var userId = _userManager.GetUserId(User); if (string.IsNullOrEmpty(userId)) return Forbid(); - var store = await _Repo.FindStore(storeId); + var store = await _storeRepo.FindStore(storeId); if (store is null) return NotFound(); @@ -171,7 +160,7 @@ namespace BTCPayServer.Controllers { if (User.Identity?.AuthenticationType != AuthenticationSchemes.Cookie) return null; - return _UserManager.GetUserId(User); + return _userManager.GetUserId(User); } } } From c7a8523b77349cc18d6ee5964667bc294b2a2747 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Thu, 4 Apr 2024 10:58:47 +0200 Subject: [PATCH 3/4] Code cleanups --- .../UIStoresController.Dashboard.cs | 6 ++-- .../Controllers/UIStoresController.Email.cs | 6 ++-- .../UIStoresController.Integrations.cs | 6 ++-- .../UIStoresController.LightningLike.cs | 11 +++--- .../Controllers/UIStoresController.Onchain.cs | 15 ++++---- .../Controllers/UIStoresController.Rates.cs | 35 +++++++++---------- .../Controllers/UIStoresController.Roles.cs | 4 +-- .../UIStoresController.Settings.cs | 8 ++--- .../Controllers/UIStoresController.Tokens.cs | 28 ++++++--------- .../Controllers/UIStoresController.cs | 4 +-- 10 files changed, 56 insertions(+), 67 deletions(-) diff --git a/BTCPayServer/Controllers/UIStoresController.Dashboard.cs b/BTCPayServer/Controllers/UIStoresController.Dashboard.cs index 3199c62fe..4d8dc01f8 100644 --- a/BTCPayServer/Controllers/UIStoresController.Dashboard.cs +++ b/BTCPayServer/Controllers/UIStoresController.Dashboard.cs @@ -121,15 +121,15 @@ namespace BTCPayServer.Controllers var derivationByCryptoCode = store .GetPaymentMethodConfigs(_handlers) - .ToDictionary(c => ((IHasNetwork)_handlers[c.Key]).Network.CryptoCode, c => (DerivationSchemeSettings)c.Value); + .ToDictionary(c => ((IHasNetwork)_handlers[c.Key]).Network.CryptoCode, c => c.Value); var lightningByCryptoCode = store .GetPaymentMethodConfigs(_handlers) .Where(c => c.Value is LightningPaymentMethodConfig) .ToDictionary(c => ((IHasNetwork)_handlers[c.Key]).Network.CryptoCode, c => (LightningPaymentMethodConfig)c.Value); - derivationSchemes = new List(); - lightningNodes = new List(); + derivationSchemes = []; + lightningNodes = []; foreach (var handler in _handlers) { diff --git a/BTCPayServer/Controllers/UIStoresController.Email.cs b/BTCPayServer/Controllers/UIStoresController.Email.cs index 7e02bf59d..4fc630d2b 100644 --- a/BTCPayServer/Controllers/UIStoresController.Email.cs +++ b/BTCPayServer/Controllers/UIStoresController.Email.cs @@ -48,7 +48,7 @@ namespace BTCPayServer.Controllers [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] public async Task StoreEmails(string storeId, StoreEmailRuleViewModel vm, string command) { - vm.Rules ??= new List(); + vm.Rules ??= []; int commandIndex = 0; var indSep = command.Split(':', StringSplitOptions.RemoveEmptyEntries); @@ -72,8 +72,8 @@ namespace BTCPayServer.Controllers { var rule = vm.Rules[i]; - if (!string.IsNullOrEmpty(rule.To) && (rule.To.Split(',', StringSplitOptions.RemoveEmptyEntries) - .Any(s => !MailboxAddressValidator.TryParse(s, out _)))) + if (!string.IsNullOrEmpty(rule.To) && rule.To.Split(',', StringSplitOptions.RemoveEmptyEntries) + .Any(s => !MailboxAddressValidator.TryParse(s, out _))) { ModelState.AddModelError($"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}", "Invalid mailbox address provided. Valid formats are: 'test@example.com' or 'Firstname Lastname '"); diff --git a/BTCPayServer/Controllers/UIStoresController.Integrations.cs b/BTCPayServer/Controllers/UIStoresController.Integrations.cs index 79da255ac..f5582afd1 100644 --- a/BTCPayServer/Controllers/UIStoresController.Integrations.cs +++ b/BTCPayServer/Controllers/UIStoresController.Integrations.cs @@ -26,7 +26,7 @@ namespace BTCPayServer.Controllers public async Task Webhooks() { var webhooks = await _storeRepo.GetWebhooks(CurrentStore.Id); - return View(nameof(Webhooks), new WebhooksViewModel() + return View(nameof(Webhooks), new WebhooksViewModel { Webhooks = webhooks.Select(async w => { @@ -146,11 +146,11 @@ namespace BTCPayServer.Controllers if (result.Success) { - TempData[WellKnownTempData.SuccessMessage] = $"{viewModel.Type.ToString()} event delivered successfully! Delivery ID is {result.DeliveryId}"; + TempData[WellKnownTempData.SuccessMessage] = $"{viewModel.Type} event delivered successfully! Delivery ID is {result.DeliveryId}"; } else { - TempData[WellKnownTempData.ErrorMessage] = $"{viewModel.Type.ToString()} event could not be delivered. Error message received: {(result.ErrorMessage ?? "unknown")}"; + TempData[WellKnownTempData.ErrorMessage] = $"{viewModel.Type} event could not be delivered. Error message received: {(result.ErrorMessage ?? "unknown")}"; } return View(nameof(TestWebhook)); diff --git a/BTCPayServer/Controllers/UIStoresController.LightningLike.cs b/BTCPayServer/Controllers/UIStoresController.LightningLike.cs index b058867ae..a442f15a8 100644 --- a/BTCPayServer/Controllers/UIStoresController.LightningLike.cs +++ b/BTCPayServer/Controllers/UIStoresController.LightningLike.cs @@ -122,7 +122,7 @@ namespace BTCPayServer.Controllers var paymentMethodId = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode); - LightningPaymentMethodConfig? paymentMethod = null; + LightningPaymentMethodConfig? paymentMethod; if (vm.LightningNodeType == LightningNodeType.Internal) { paymentMethod = new LightningPaymentMethodConfig(); @@ -135,8 +135,7 @@ namespace BTCPayServer.Controllers ModelState.AddModelError(nameof(vm.ConnectionString), "Please provide a connection string"); return View(vm); } - paymentMethod = new LightningPaymentMethodConfig(); - paymentMethod.ConnectionString = vm.ConnectionString; + paymentMethod = new LightningPaymentMethodConfig { ConnectionString = vm.ConnectionString }; } var handler = (LightningLikePaymentHandler)_handlers[paymentMethodId]; @@ -153,7 +152,7 @@ namespace BTCPayServer.Controllers case "save": var lnurl = PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode); store.SetPaymentMethodConfig(_handlers[paymentMethodId], paymentMethod); - store.SetPaymentMethodConfig(_handlers[lnurl], new LNURLPaymentMethodConfig() + store.SetPaymentMethodConfig(_handlers[lnurl], new LNURLPaymentMethodConfig { UseBech32Scheme = true, LUD12Enabled = false @@ -293,10 +292,10 @@ namespace BTCPayServer.Controllers if (store == null) return NotFound(); - if (cryptoCode == null) + var network = _explorerProvider.GetNetwork(cryptoCode); + if (network == null) return NotFound(); - var network = _explorerProvider.GetNetwork(cryptoCode); var lightning = GetConfig(PaymentTypes.LN.GetPaymentMethodId(cryptoCode), store); if (lightning == null) return NotFound(); diff --git a/BTCPayServer/Controllers/UIStoresController.Onchain.cs b/BTCPayServer/Controllers/UIStoresController.Onchain.cs index 70ff48d33..17661d00e 100644 --- a/BTCPayServer/Controllers/UIStoresController.Onchain.cs +++ b/BTCPayServer/Controllers/UIStoresController.Onchain.cs @@ -103,7 +103,9 @@ namespace BTCPayServer.Controllers } catch { + // ignored } + if (fileContent is null || !_onChainWalletParsers.TryParseWalletFile(fileContent, network, out strategy, out _)) { ModelState.AddModelError(nameof(vm.WalletFile), $"Import failed, make sure you import a compatible wallet format"); @@ -430,7 +432,7 @@ namespace BTCPayServer.Controllers .Select(e => new WalletSettingsAccountKeyViewModel { AccountKey = e.AccountKey.ToString(), - MasterFingerprint = e.RootFingerprint is HDFingerprint fp ? fp.ToString() : null, + MasterFingerprint = e.RootFingerprint is { } fp ? fp.ToString() : null, AccountKeyPath = e.AccountKeyPath == null ? "" : $"m/{e.AccountKeyPath}" }).ToList(), Config = ProtectString(JToken.FromObject(derivation, handler.Serializer).ToString()), @@ -521,7 +523,7 @@ namespace BTCPayServer.Controllers try { rootFingerprint = string.IsNullOrWhiteSpace(vm.AccountKeys[i].MasterFingerprint) - ? (HDFingerprint?)null + ? null : new HDFingerprint(Encoders.Hex.DecodeData(vm.AccountKeys[i].MasterFingerprint)); if (rootFingerprint != null && derivation.AccountKeySettings[i].RootFingerprint != rootFingerprint) @@ -603,17 +605,16 @@ namespace BTCPayServer.Controllers TempData[WellKnownTempData.SuccessMessage] = "Payment settings successfully updated"; - if (payjoinChanged && blob.PayJoinEnabled && network.SupportPayJoin) { var config = store.GetPaymentMethodConfig(PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode), _handlers); - if (!config.IsHotWallet) + if (config?.IsHotWallet is not true) { TempData.Remove(WellKnownTempData.SuccessMessage); - TempData.SetStatusMessageModel(new StatusMessageModel() + TempData.SetStatusMessageModel(new StatusMessageModel { Severity = StatusMessageModel.StatusSeverity.Warning, - Html = $"The payment settings were updated successfully. However, PayJoin will not work, as this isn't a hot wallet." + Html = "The payment settings were updated successfully. However, PayJoin will not work, as this isn't a hot wallet." }); } } @@ -798,7 +799,7 @@ namespace BTCPayServer.Controllers private async Task GetSeed(ExplorerClient client, DerivationSchemeSettings derivation) { return derivation.IsHotWallet && - await client.GetMetadataAsync(derivation.AccountDerivation, WellknownMetadataKeys.MasterHDKey) is string seed && + await client.GetMetadataAsync(derivation.AccountDerivation, WellknownMetadataKeys.MasterHDKey) is { } seed && !string.IsNullOrEmpty(seed) ? seed : null; } diff --git a/BTCPayServer/Controllers/UIStoresController.Rates.cs b/BTCPayServer/Controllers/UIStoresController.Rates.cs index 30f1e323d..237516bb1 100644 --- a/BTCPayServer/Controllers/UIStoresController.Rates.cs +++ b/BTCPayServer/Controllers/UIStoresController.Rates.cs @@ -21,7 +21,7 @@ namespace BTCPayServer.Controllers [HttpGet("{storeId}/rates")] public IActionResult Rates() { - var exchanges = GetSupportedExchanges(); + var exchanges = GetSupportedExchanges().ToList(); var storeBlob = CurrentStore.GetStoreBlob(); var vm = new RatesViewModel(); vm.SetExchangeRates(exchanges, storeBlob.PreferredExchange ?? storeBlob.GetRecommendedExchange()); @@ -43,13 +43,13 @@ namespace BTCPayServer.Controllers { return RedirectToAction(nameof(ShowRateRules), new { scripting = true, storeId = model.StoreId }); } - else if (command == "scripting-off") + if (command == "scripting-off") { return RedirectToAction(nameof(ShowRateRules), new { scripting = false, storeId = model.StoreId }); } - var exchanges = GetSupportedExchanges(); - model.SetExchangeRates(exchanges, model.PreferredExchange ?? this.HttpContext.GetStoreData().GetStoreBlob().GetRecommendedExchange()); + var exchanges = GetSupportedExchanges().ToList(); + model.SetExchangeRates(exchanges, model.PreferredExchange ?? HttpContext.GetStoreData().GetStoreBlob().GetRecommendedExchange()); model.StoreId = storeId ?? model.StoreId; CurrencyPair[]? currencyPairs = null; try @@ -85,13 +85,13 @@ namespace BTCPayServer.Controllers return View(model); } } - RateRules? rules = null; + RateRules? rules; if (model.ShowScripting) { if (!RateRules.TryParse(model.Script, out rules, out var errors)) { - errors = errors ?? new List(); - var errorString = String.Join(", ", errors.ToArray()); + errors ??= []; + var errorString = string.Join(", ", errors.ToArray()); ModelState.AddModelError(nameof(model.Script), $"Parsing error ({errorString})"); return View(model); } @@ -129,7 +129,7 @@ namespace BTCPayServer.Controllers foreach (var fetch in fetchs) { var testResult = await (fetch.Value); - testResults.Add(new RatesViewModel.TestResultViewModel() + testResults.Add(new RatesViewModel.TestResultViewModel { CurrencyPair = fetch.Key.ToString(), Error = testResult.Errors.Count != 0, @@ -140,18 +140,17 @@ namespace BTCPayServer.Controllers model.TestRateRules = testResults; return View(model); } - else // command == Save + + // command == Save + if (CurrentStore.SetStoreBlob(blob)) { - if (CurrentStore.SetStoreBlob(blob)) - { - await _storeRepo.UpdateStore(CurrentStore); - TempData[WellKnownTempData.SuccessMessage] = "Rate settings updated"; - } - return RedirectToAction(nameof(Rates), new - { - storeId = CurrentStore.Id - }); + await _storeRepo.UpdateStore(CurrentStore); + TempData[WellKnownTempData.SuccessMessage] = "Rate settings updated"; } + return RedirectToAction(nameof(Rates), new + { + storeId = CurrentStore.Id + }); } [HttpGet("{storeId}/rates/confirm")] diff --git a/BTCPayServer/Controllers/UIStoresController.Roles.cs b/BTCPayServer/Controllers/UIStoresController.Roles.cs index 2e249daa5..4d20fbd78 100644 --- a/BTCPayServer/Controllers/UIStoresController.Roles.cs +++ b/BTCPayServer/Controllers/UIStoresController.Roles.cs @@ -73,7 +73,7 @@ namespace BTCPayServer.Controllers [FromServices] StoreRepository storeRepository, [FromRoute] string role, UpdateRoleViewModel viewModel) { - string successMessage = null; + string successMessage; StoreRoleId roleId; if (role == "create") { @@ -122,7 +122,7 @@ namespace BTCPayServer.Controllers [FromServices] StoreRepository storeRepository, string role) { - var roleData = await storeRepository.GetStoreRole(new StoreRoleId(storeId, role), true);; + var roleData = await storeRepository.GetStoreRole(new StoreRoleId(storeId, role), true); if (roleData == null) return NotFound(); diff --git a/BTCPayServer/Controllers/UIStoresController.Settings.cs b/BTCPayServer/Controllers/UIStoresController.Settings.cs index 7e194ec9d..cb037e163 100644 --- a/BTCPayServer/Controllers/UIStoresController.Settings.cs +++ b/BTCPayServer/Controllers/UIStoresController.Settings.cs @@ -18,7 +18,7 @@ namespace BTCPayServer.Controllers { public partial class UIStoresController { - [HttpGet("{storeId}/settings")] + [HttpGet("{storeId}/settings")] public IActionResult GeneralSettings() { var store = HttpContext.GetStoreData(); @@ -358,7 +358,7 @@ namespace BTCPayServer.Controllers { var paymentMethodId = PaymentMethodId.Parse(newCriteria.PaymentMethod); if (_handlers.TryGet(paymentMethodId) is LightningLikePaymentHandler h) - model.PaymentMethodCriteria.Add(new PaymentMethodCriteriaViewModel() + model.PaymentMethodCriteria.Add(new PaymentMethodCriteriaViewModel { PaymentMethod = PaymentTypes.LNURL.GetPaymentMethodId(h.Network.CryptoCode).ToString(), Type = newCriteria.Type, @@ -419,7 +419,7 @@ namespace BTCPayServer.Controllers }); } - void SetCryptoCurrencies(CheckoutAppearanceViewModel vm, Data.StoreData storeData) + void SetCryptoCurrencies(CheckoutAppearanceViewModel vm, StoreData storeData) { var choices = GetEnabledPaymentMethodChoices(storeData); var chosen = GetDefaultPaymentMethodChoice(storeData); @@ -432,7 +432,7 @@ namespace BTCPayServer.Controllers { var enabled = storeData.GetEnabledPaymentIds(); var defaultPaymentId = storeData.GetDefaultPaymentId(); - var defaultChoice = defaultPaymentId is not null ? defaultPaymentId.FindNearest(enabled) : null; + var defaultChoice = defaultPaymentId?.FindNearest(enabled); if (defaultChoice is null) { defaultChoice = enabled.FirstOrDefault(e => e == PaymentTypes.CHAIN.GetPaymentMethodId(_networkProvider.DefaultNetwork.CryptoCode)) ?? diff --git a/BTCPayServer/Controllers/UIStoresController.Tokens.cs b/BTCPayServer/Controllers/UIStoresController.Tokens.cs index 8093c7788..c943683cd 100644 --- a/BTCPayServer/Controllers/UIStoresController.Tokens.cs +++ b/BTCPayServer/Controllers/UIStoresController.Tokens.cs @@ -33,10 +33,7 @@ namespace BTCPayServer.Controllers }).ToArray(); model.ApiKey = (await _tokenRepository.GetLegacyAPIKeys(CurrentStore.Id)).FirstOrDefault(); - if (model.ApiKey == null) - model.EncodedApiKey = "*API Key*"; - else - model.EncodedApiKey = Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(model.ApiKey)); + model.EncodedApiKey = model.ApiKey == null ? "*API Key*" : Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(model.ApiKey)); return View(model); } @@ -94,7 +91,7 @@ namespace BTCPayServer.Controllers { return View(nameof(CreateToken), model); } - model.Label = model.Label ?? string.Empty; + model.Label ??= string.Empty; var userId = GetUserId(); if (userId == null) return Challenge(AuthenticationSchemes.Cookie); @@ -111,7 +108,7 @@ namespace BTCPayServer.Controllers Id = model.PublicKey == null ? null : NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(new PubKey(model.PublicKey).Compress()) }; - string? pairingCode = null; + string? pairingCode; if (model.PublicKey == null) { tokenRequest.PairingCode = await _tokenRepository.CreatePairingCodeAsync(); @@ -245,26 +242,21 @@ namespace BTCPayServer.Controllers if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial) { var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods(); - StoreNotConfigured = !store.GetPaymentMethodConfigs(_handlers) - .Where(p => !excludeFilter.Match(p.Key)) - .Any(); + StoreNotConfigured = store.GetPaymentMethodConfigs(_handlers).All(p => excludeFilter.Match(p.Key)); TempData[WellKnownTempData.SuccessMessage] = "Pairing is successful"; if (pairingResult == PairingResult.Partial) TempData[WellKnownTempData.SuccessMessage] = "Server initiated pairing code: " + pairingCode; return RedirectToAction(nameof(ListTokens), new { - storeId = store.Id, - pairingCode = pairingCode + storeId = store.Id, pairingCode }); } - else + + TempData[WellKnownTempData.ErrorMessage] = $"Pairing failed ({pairingResult})"; + return RedirectToAction(nameof(ListTokens), new { - TempData[WellKnownTempData.ErrorMessage] = $"Pairing failed ({pairingResult})"; - return RedirectToAction(nameof(ListTokens), new - { - storeId = store.Id - }); - } + storeId = store.Id + }); } } } diff --git a/BTCPayServer/Controllers/UIStoresController.cs b/BTCPayServer/Controllers/UIStoresController.cs index 4e98be73e..fbb7cf328 100644 --- a/BTCPayServer/Controllers/UIStoresController.cs +++ b/BTCPayServer/Controllers/UIStoresController.cs @@ -158,9 +158,7 @@ namespace BTCPayServer.Controllers private string? GetUserId() { - if (User.Identity?.AuthenticationType != AuthenticationSchemes.Cookie) - return null; - return _userManager.GetUserId(User); + return User.Identity?.AuthenticationType != AuthenticationSchemes.Cookie ? null : _userManager.GetUserId(User); } } } From 620ebc751c0bd323226157ce90da4dc8521df580 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Thu, 4 Apr 2024 11:00:18 +0200 Subject: [PATCH 4/4] Convert to file-scoped namespace --- .../UIStoresController.Dashboard.cs | 241 ++- .../Controllers/UIStoresController.Email.cs | 429 +++-- .../UIStoresController.Integrations.cs | 311 ++-- .../UIStoresController.LightningLike.cs | 577 ++++--- .../Controllers/UIStoresController.Onchain.cs | 1499 ++++++++--------- .../Controllers/UIStoresController.Rates.cs | 299 ++-- .../Controllers/UIStoresController.Roles.cs | 249 ++- .../UIStoresController.Settings.cs | 681 ++++---- .../Controllers/UIStoresController.Tokens.cs | 439 +++-- .../Controllers/UIStoresController.Users.cs | 219 ++- .../Controllers/UIStoresController.cs | 247 ++- 11 files changed, 2590 insertions(+), 2601 deletions(-) diff --git a/BTCPayServer/Controllers/UIStoresController.Dashboard.cs b/BTCPayServer/Controllers/UIStoresController.Dashboard.cs index 4d8dc01f8..1cf1b178d 100644 --- a/BTCPayServer/Controllers/UIStoresController.Dashboard.cs +++ b/BTCPayServer/Controllers/UIStoresController.Dashboard.cs @@ -16,156 +16,155 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NBitcoin; -namespace BTCPayServer.Controllers +namespace BTCPayServer.Controllers; + +public partial class UIStoresController { - public partial class UIStoresController + [HttpGet("{storeId}")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task Dashboard() { - [HttpGet("{storeId}")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task Dashboard() - { - var store = CurrentStore; - if (store is null) - return NotFound(); + var store = CurrentStore; + if (store is null) + return NotFound(); - var storeBlob = store.GetStoreBlob(); + var storeBlob = store.GetStoreBlob(); - AddPaymentMethods(store, storeBlob, - out var derivationSchemes, out var lightningNodes); + AddPaymentMethods(store, storeBlob, + out var derivationSchemes, out var lightningNodes); - var walletEnabled = derivationSchemes.Any(scheme => !string.IsNullOrEmpty(scheme.Value) && scheme.Enabled); - var lightningEnabled = lightningNodes.Any(ln => !string.IsNullOrEmpty(ln.Address) && ln.Enabled); - var cryptoCode = _networkProvider.DefaultNetwork.CryptoCode; - var vm = new StoreDashboardViewModel - { - WalletEnabled = walletEnabled, - LightningEnabled = lightningEnabled, - LightningSupported = _networkProvider.GetNetwork(cryptoCode)?.SupportLightning is true, - StoreId = CurrentStore.Id, - StoreName = CurrentStore.StoreName, - CryptoCode = cryptoCode, - Network = _networkProvider.DefaultNetwork, - IsSetUp = walletEnabled || lightningEnabled - }; - - // Widget data - if (vm is { WalletEnabled: false, LightningEnabled: false }) - return View(vm); - - var userId = GetUserId(); - if (userId is null) - return NotFound(); - - var apps = await _appService.GetAllApps(userId, false, store.Id); - foreach (var app in apps) - { - var appData = await _appService.GetAppData(userId, app.Id); - vm.Apps.Add(appData); - } + var walletEnabled = derivationSchemes.Any(scheme => !string.IsNullOrEmpty(scheme.Value) && scheme.Enabled); + var lightningEnabled = lightningNodes.Any(ln => !string.IsNullOrEmpty(ln.Address) && ln.Enabled); + var cryptoCode = _networkProvider.DefaultNetwork.CryptoCode; + var vm = new StoreDashboardViewModel + { + WalletEnabled = walletEnabled, + LightningEnabled = lightningEnabled, + LightningSupported = _networkProvider.GetNetwork(cryptoCode)?.SupportLightning is true, + StoreId = CurrentStore.Id, + StoreName = CurrentStore.StoreName, + CryptoCode = cryptoCode, + Network = _networkProvider.DefaultNetwork, + IsSetUp = walletEnabled || lightningEnabled + }; + // Widget data + if (vm is { WalletEnabled: false, LightningEnabled: false }) return View(vm); + + var userId = GetUserId(); + if (userId is null) + return NotFound(); + + var apps = await _appService.GetAllApps(userId, false, store.Id); + foreach (var app in apps) + { + var appData = await _appService.GetAppData(userId, app.Id); + vm.Apps.Add(appData); } - [HttpGet("{storeId}/dashboard/{cryptoCode}/lightning/balance")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public IActionResult LightningBalance(string storeId, string cryptoCode) - { - var store = HttpContext.GetStoreData(); - if (store == null) - return NotFound(); + return View(vm); + } - var vm = new StoreLightningBalanceViewModel { Store = store, CryptoCode = cryptoCode }; - return ViewComponent("StoreLightningBalance", new { vm }); - } + [HttpGet("{storeId}/dashboard/{cryptoCode}/lightning/balance")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public IActionResult LightningBalance(string storeId, string cryptoCode) + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); - [HttpGet("{storeId}/dashboard/{cryptoCode}/numbers")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public IActionResult StoreNumbers(string storeId, string cryptoCode) - { - var store = HttpContext.GetStoreData(); - if (store == null) - return NotFound(); + var vm = new StoreLightningBalanceViewModel { Store = store, CryptoCode = cryptoCode }; + return ViewComponent("StoreLightningBalance", new { vm }); + } - var vm = new StoreNumbersViewModel { Store = store, CryptoCode = cryptoCode }; - return ViewComponent("StoreNumbers", new { vm }); - } + [HttpGet("{storeId}/dashboard/{cryptoCode}/numbers")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public IActionResult StoreNumbers(string storeId, string cryptoCode) + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); - [HttpGet("{storeId}/dashboard/{cryptoCode}/recent-transactions")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public IActionResult RecentTransactions(string storeId, string cryptoCode) - { - var store = HttpContext.GetStoreData(); - if (store == null) - return NotFound(); + var vm = new StoreNumbersViewModel { Store = store, CryptoCode = cryptoCode }; + return ViewComponent("StoreNumbers", new { vm }); + } - var vm = new StoreRecentTransactionsViewModel { Store = store, CryptoCode = cryptoCode }; - return ViewComponent("StoreRecentTransactions", new { vm }); - } + [HttpGet("{storeId}/dashboard/{cryptoCode}/recent-transactions")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public IActionResult RecentTransactions(string storeId, string cryptoCode) + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); - [HttpGet("{storeId}/dashboard/{cryptoCode}/recent-invoices")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public IActionResult RecentInvoices(string storeId, string cryptoCode) - { - var store = HttpContext.GetStoreData(); - if (store == null) - return NotFound(); + var vm = new StoreRecentTransactionsViewModel { Store = store, CryptoCode = cryptoCode }; + return ViewComponent("StoreRecentTransactions", new { vm }); + } - var vm = new StoreRecentInvoicesViewModel { Store = store, CryptoCode = cryptoCode }; - return ViewComponent("StoreRecentInvoices", new { vm }); - } + [HttpGet("{storeId}/dashboard/{cryptoCode}/recent-invoices")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public IActionResult RecentInvoices(string storeId, string cryptoCode) + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); - internal void AddPaymentMethods(StoreData store, StoreBlob storeBlob, - out List derivationSchemes, out List lightningNodes) - { - var excludeFilters = storeBlob.GetExcludedPaymentMethods(); - var derivationByCryptoCode = - store + var vm = new StoreRecentInvoicesViewModel { Store = store, CryptoCode = cryptoCode }; + return ViewComponent("StoreRecentInvoices", new { vm }); + } + + internal void AddPaymentMethods(StoreData store, StoreBlob storeBlob, + out List derivationSchemes, out List lightningNodes) + { + var excludeFilters = storeBlob.GetExcludedPaymentMethods(); + var derivationByCryptoCode = + store .GetPaymentMethodConfigs(_handlers) .ToDictionary(c => ((IHasNetwork)_handlers[c.Key]).Network.CryptoCode, c => c.Value); - var lightningByCryptoCode = store - .GetPaymentMethodConfigs(_handlers) - .Where(c => c.Value is LightningPaymentMethodConfig) - .ToDictionary(c => ((IHasNetwork)_handlers[c.Key]).Network.CryptoCode, c => (LightningPaymentMethodConfig)c.Value); + var lightningByCryptoCode = store + .GetPaymentMethodConfigs(_handlers) + .Where(c => c.Value is LightningPaymentMethodConfig) + .ToDictionary(c => ((IHasNetwork)_handlers[c.Key]).Network.CryptoCode, c => (LightningPaymentMethodConfig)c.Value); - derivationSchemes = []; - lightningNodes = []; + derivationSchemes = []; + lightningNodes = []; - foreach (var handler in _handlers) + foreach (var handler in _handlers) + { + if (handler is BitcoinLikePaymentHandler { Network: var network }) { - if (handler is BitcoinLikePaymentHandler { Network: var network }) - { - var strategy = derivationByCryptoCode.TryGet(network.CryptoCode); - var value = strategy?.ToPrettyString() ?? string.Empty; + var strategy = derivationByCryptoCode.TryGet(network.CryptoCode); + var value = strategy?.ToPrettyString() ?? string.Empty; - derivationSchemes.Add(new StoreDerivationScheme - { - Crypto = network.CryptoCode, - PaymentMethodId = handler.PaymentMethodId, - WalletSupported = network.WalletSupported, - Value = value, - WalletId = new WalletId(store.Id, network.CryptoCode), - Enabled = !excludeFilters.Match(handler.PaymentMethodId) && strategy != null, + derivationSchemes.Add(new StoreDerivationScheme + { + Crypto = network.CryptoCode, + PaymentMethodId = handler.PaymentMethodId, + WalletSupported = network.WalletSupported, + Value = value, + WalletId = new WalletId(store.Id, network.CryptoCode), + Enabled = !excludeFilters.Match(handler.PaymentMethodId) && strategy != null, #if ALTCOINS Collapsed = network is Plugins.Altcoins.ElementsBTCPayNetwork elementsBTCPayNetwork && elementsBTCPayNetwork.NetworkCryptoCode != elementsBTCPayNetwork.CryptoCode && string.IsNullOrEmpty(value) #endif - }); - } - else if (handler is LightningLikePaymentHandler) + }); + } + else if (handler is LightningLikePaymentHandler) + { + var lnNetwork = ((IHasNetwork)handler).Network; + var lightning = lightningByCryptoCode.TryGet(lnNetwork.CryptoCode); + var isEnabled = !excludeFilters.Match(handler.PaymentMethodId) && lightning != null; + lightningNodes.Add(new StoreLightningNode { - var lnNetwork = ((IHasNetwork)handler).Network; - var lightning = lightningByCryptoCode.TryGet(lnNetwork.CryptoCode); - var isEnabled = !excludeFilters.Match(handler.PaymentMethodId) && lightning != null; - lightningNodes.Add(new StoreLightningNode - { - CryptoCode = lnNetwork.CryptoCode, - PaymentMethodId = handler.PaymentMethodId, - Address = lightning?.GetDisplayableConnectionString(), - Enabled = isEnabled - }); - } + CryptoCode = lnNetwork.CryptoCode, + PaymentMethodId = handler.PaymentMethodId, + Address = lightning?.GetDisplayableConnectionString(), + Enabled = isEnabled + }); } } - } + } diff --git a/BTCPayServer/Controllers/UIStoresController.Email.cs b/BTCPayServer/Controllers/UIStoresController.Email.cs index 4fc630d2b..e25f23ffb 100644 --- a/BTCPayServer/Controllers/UIStoresController.Email.cs +++ b/BTCPayServer/Controllers/UIStoresController.Email.cs @@ -15,255 +15,254 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MimeKit; -namespace BTCPayServer.Controllers +namespace BTCPayServer.Controllers; + +public partial class UIStoresController { - public partial class UIStoresController + [HttpGet("{storeId}/emails")] + public async Task StoreEmails(string storeId) { - [HttpGet("{storeId}/emails")] - public async Task StoreEmails(string storeId) + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + + var blob = store.GetStoreBlob(); + if (blob.EmailSettings?.IsComplete() is not true && !TempData.HasStatusMessage()) { - var store = HttpContext.GetStoreData(); - if (store == null) - return NotFound(); - - var blob = store.GetStoreBlob(); - if (blob.EmailSettings?.IsComplete() is not true && !TempData.HasStatusMessage()) - { - var emailSender = await _emailSenderFactory.GetEmailSender(store.Id) as StoreEmailSender; - if (!await IsSetupComplete(emailSender?.FallbackSender)) - { - TempData.SetStatusMessageModel(new StatusMessageModel - { - Severity = StatusMessageModel.StatusSeverity.Warning, - Html = $"You need to configure email settings before this feature works. Configure store email settings." - }); - } - } - - var vm = new StoreEmailRuleViewModel { Rules = blob.EmailRules ?? [] }; - return View(vm); - } - - [HttpPost("{storeId}/emails")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task StoreEmails(string storeId, StoreEmailRuleViewModel vm, string command) - { - vm.Rules ??= []; - int commandIndex = 0; - - var indSep = command.Split(':', StringSplitOptions.RemoveEmptyEntries); - if (indSep.Length > 1) - { - commandIndex = int.Parse(indSep[1], CultureInfo.InvariantCulture); - } - - if (command.StartsWith("remove", StringComparison.InvariantCultureIgnoreCase)) - { - vm.Rules.RemoveAt(commandIndex); - } - else if (command == "add") - { - vm.Rules.Add(new StoreEmailRule()); - - return View(vm); - } - - for (var i = 0; i < vm.Rules.Count; i++) - { - var rule = vm.Rules[i]; - - if (!string.IsNullOrEmpty(rule.To) && rule.To.Split(',', StringSplitOptions.RemoveEmptyEntries) - .Any(s => !MailboxAddressValidator.TryParse(s, out _))) - { - ModelState.AddModelError($"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}", - "Invalid mailbox address provided. Valid formats are: 'test@example.com' or 'Firstname Lastname '"); - } - else if (!rule.CustomerEmail && string.IsNullOrEmpty(rule.To)) - ModelState.AddModelError($"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}", - "Either recipient or \"Send the email to the buyer\" is required"); - } - - if (!ModelState.IsValid) - { - return View(vm); - } - - var store = HttpContext.GetStoreData(); - - if (store == null) - return NotFound(); - - string message = ""; - - // update rules - var blob = store.GetStoreBlob(); - blob.EmailRules = vm.Rules; - if (store.SetStoreBlob(blob)) - { - await _storeRepo.UpdateStore(store); - message += "Store email rules saved. "; - } - - if (command.StartsWith("test", StringComparison.InvariantCultureIgnoreCase)) - { - try - { - var rule = vm.Rules[commandIndex]; - var emailSender = await _emailSenderFactory.GetEmailSender(store.Id); - if (await IsSetupComplete(emailSender)) - { - var recipients = rule.To.Split(",", StringSplitOptions.RemoveEmptyEntries) - .Select(o => - { - MailboxAddressValidator.TryParse(o, out var mb); - return mb; - }) - .Where(o => o != null) - .ToArray(); - - emailSender.SendEmail(recipients.ToArray(), null, null, $"[TEST] {rule.Subject}", rule.Body); - message += "Test email sent — please verify you received it."; - } - else - { - message += "Complete the email setup to send test emails."; - } - } - catch (Exception ex) - { - TempData[WellKnownTempData.ErrorMessage] = message + "Error sending test email: " + ex.Message; - return RedirectToAction("StoreEmails", new { storeId }); - } - } - - if (!string.IsNullOrEmpty(message)) + var emailSender = await _emailSenderFactory.GetEmailSender(store.Id) as StoreEmailSender; + if (!await IsSetupComplete(emailSender?.FallbackSender)) { TempData.SetStatusMessageModel(new StatusMessageModel { - Severity = StatusMessageModel.StatusSeverity.Success, - Message = message + Severity = StatusMessageModel.StatusSeverity.Warning, + Html = $"You need to configure email settings before this feature works. Configure store email settings." }); } - - return RedirectToAction("StoreEmails", new { storeId }); } - public class StoreEmailRuleViewModel + var vm = new StoreEmailRuleViewModel { Rules = blob.EmailRules ?? [] }; + return View(vm); + } + + [HttpPost("{storeId}/emails")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task StoreEmails(string storeId, StoreEmailRuleViewModel vm, string command) + { + vm.Rules ??= []; + int commandIndex = 0; + + var indSep = command.Split(':', StringSplitOptions.RemoveEmptyEntries); + if (indSep.Length > 1) { - public List Rules { get; set; } + commandIndex = int.Parse(indSep[1], CultureInfo.InvariantCulture); } - public class StoreEmailRule + if (command.StartsWith("remove", StringComparison.InvariantCultureIgnoreCase)) { - [Required] - public string Trigger { get; set; } - - public bool CustomerEmail { get; set; } - - - public string To { get; set; } - - [Required] - public string Subject { get; set; } - - [Required] - public string Body { get; set; } + vm.Rules.RemoveAt(commandIndex); } - - [HttpGet("{storeId}/email-settings")] - public async Task StoreEmailSettings(string storeId) + else if (command == "add") { - var store = HttpContext.GetStoreData(); - if (store == null) - return NotFound(); + vm.Rules.Add(new StoreEmailRule()); - var blob = store.GetStoreBlob(); - var data = blob.EmailSettings ?? new EmailSettings(); - var fallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender - ? await storeSender.FallbackSender.GetEmailSettings() - : null; - var vm = new EmailsViewModel(data, fallbackSettings); - return View(vm); } - [HttpPost("{storeId}/email-settings")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task StoreEmailSettings(string storeId, EmailsViewModel model, string command, [FromForm] bool useCustomSMTP = false) + for (var i = 0; i < vm.Rules.Count; i++) { - var store = HttpContext.GetStoreData(); - if (store == null) - return NotFound(); + var rule = vm.Rules[i]; - ViewBag.UseCustomSMTP = useCustomSMTP; - model.FallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender - ? await storeSender.FallbackSender.GetEmailSettings() - : null; - if (useCustomSMTP) + if (!string.IsNullOrEmpty(rule.To) && rule.To.Split(',', StringSplitOptions.RemoveEmptyEntries) + .Any(s => !MailboxAddressValidator.TryParse(s, out _))) { - model.Settings.Validate("Settings.", ModelState); + ModelState.AddModelError($"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}", + "Invalid mailbox address provided. Valid formats are: 'test@example.com' or 'Firstname Lastname '"); } - if (command == "Test") + else if (!rule.CustomerEmail && string.IsNullOrEmpty(rule.To)) + ModelState.AddModelError($"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}", + "Either recipient or \"Send the email to the buyer\" is required"); + } + + if (!ModelState.IsValid) + { + return View(vm); + } + + var store = HttpContext.GetStoreData(); + + if (store == null) + return NotFound(); + + string message = ""; + + // update rules + var blob = store.GetStoreBlob(); + blob.EmailRules = vm.Rules; + if (store.SetStoreBlob(blob)) + { + await _storeRepo.UpdateStore(store); + message += "Store email rules saved. "; + } + + if (command.StartsWith("test", StringComparison.InvariantCultureIgnoreCase)) + { + try { - try + var rule = vm.Rules[commandIndex]; + var emailSender = await _emailSenderFactory.GetEmailSender(store.Id); + if (await IsSetupComplete(emailSender)) { - if (useCustomSMTP) - { - if (model.PasswordSet) + var recipients = rule.To.Split(",", StringSplitOptions.RemoveEmptyEntries) + .Select(o => { - model.Settings.Password = store.GetStoreBlob().EmailSettings.Password; - } + MailboxAddressValidator.TryParse(o, out var mb); + return mb; + }) + .Where(o => o != null) + .ToArray(); + + emailSender.SendEmail(recipients.ToArray(), null, null, $"[TEST] {rule.Subject}", rule.Body); + message += "Test email sent — please verify you received it."; + } + else + { + message += "Complete the email setup to send test emails."; + } + } + catch (Exception ex) + { + TempData[WellKnownTempData.ErrorMessage] = message + "Error sending test email: " + ex.Message; + return RedirectToAction("StoreEmails", new { storeId }); + } + } + + if (!string.IsNullOrEmpty(message)) + { + TempData.SetStatusMessageModel(new StatusMessageModel + { + Severity = StatusMessageModel.StatusSeverity.Success, + Message = message + }); + } + + return RedirectToAction("StoreEmails", new { storeId }); + } + + public class StoreEmailRuleViewModel + { + public List Rules { get; set; } + } + + public class StoreEmailRule + { + [Required] + public string Trigger { get; set; } + + public bool CustomerEmail { get; set; } + + + public string To { get; set; } + + [Required] + public string Subject { get; set; } + + [Required] + public string Body { get; set; } + } + + [HttpGet("{storeId}/email-settings")] + public async Task StoreEmailSettings(string storeId) + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + + var blob = store.GetStoreBlob(); + var data = blob.EmailSettings ?? new EmailSettings(); + var fallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender + ? await storeSender.FallbackSender.GetEmailSettings() + : null; + var vm = new EmailsViewModel(data, fallbackSettings); + + return View(vm); + } + + [HttpPost("{storeId}/email-settings")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task StoreEmailSettings(string storeId, EmailsViewModel model, string command, [FromForm] bool useCustomSMTP = false) + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + + ViewBag.UseCustomSMTP = useCustomSMTP; + model.FallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender + ? await storeSender.FallbackSender.GetEmailSettings() + : null; + if (useCustomSMTP) + { + model.Settings.Validate("Settings.", ModelState); + } + if (command == "Test") + { + try + { + if (useCustomSMTP) + { + if (model.PasswordSet) + { + model.Settings.Password = store.GetStoreBlob().EmailSettings.Password; } + } - if (string.IsNullOrEmpty(model.TestEmail)) - ModelState.AddModelError(nameof(model.TestEmail), new RequiredAttribute().FormatErrorMessage(nameof(model.TestEmail))); - if (!ModelState.IsValid) - return View(model); - var settings = useCustomSMTP ? model.Settings : model.FallbackSettings; - using var client = await settings.CreateSmtpClient(); - var message = settings.CreateMailMessage(MailboxAddress.Parse(model.TestEmail), $"{store.StoreName}: Email test", "You received it, the BTCPay Server SMTP settings work.", false); - await client.SendAsync(message); - await client.DisconnectAsync(true); - TempData[WellKnownTempData.SuccessMessage] = $"Email sent to {model.TestEmail}. Please verify you received it."; - } - catch (Exception ex) - { - TempData[WellKnownTempData.ErrorMessage] = "Error: " + ex.Message; - } - return View(model); - } - if (command == "ResetPassword") - { - var storeBlob = store.GetStoreBlob(); - storeBlob.EmailSettings.Password = null; - store.SetStoreBlob(storeBlob); - await _storeRepo.UpdateStore(store); - TempData[WellKnownTempData.SuccessMessage] = "Email server password reset"; - } - if (useCustomSMTP) - { - if (model.Settings.From is not null && !MailboxAddressValidator.IsMailboxAddress(model.Settings.From)) - { - ModelState.AddModelError("Settings.From", "Invalid email"); - } + if (string.IsNullOrEmpty(model.TestEmail)) + ModelState.AddModelError(nameof(model.TestEmail), new RequiredAttribute().FormatErrorMessage(nameof(model.TestEmail))); if (!ModelState.IsValid) return View(model); - var storeBlob = store.GetStoreBlob(); - if (storeBlob.EmailSettings != null && new EmailsViewModel(storeBlob.EmailSettings, model.FallbackSettings).PasswordSet) - { - model.Settings.Password = storeBlob.EmailSettings.Password; - } - storeBlob.EmailSettings = model.Settings; - store.SetStoreBlob(storeBlob); - await _storeRepo.UpdateStore(store); - TempData[WellKnownTempData.SuccessMessage] = "Email settings modified"; + var settings = useCustomSMTP ? model.Settings : model.FallbackSettings; + using var client = await settings.CreateSmtpClient(); + var message = settings.CreateMailMessage(MailboxAddress.Parse(model.TestEmail), $"{store.StoreName}: Email test", "You received it, the BTCPay Server SMTP settings work.", false); + await client.SendAsync(message); + await client.DisconnectAsync(true); + TempData[WellKnownTempData.SuccessMessage] = $"Email sent to {model.TestEmail}. Please verify you received it."; } - return RedirectToAction(nameof(StoreEmailSettings), new { storeId }); + catch (Exception ex) + { + TempData[WellKnownTempData.ErrorMessage] = "Error: " + ex.Message; + } + return View(model); } - - private static async Task IsSetupComplete(IEmailSender emailSender) + if (command == "ResetPassword") { - return emailSender is not null && (await emailSender.GetEmailSettings())?.IsComplete() == true; + var storeBlob = store.GetStoreBlob(); + storeBlob.EmailSettings.Password = null; + store.SetStoreBlob(storeBlob); + await _storeRepo.UpdateStore(store); + TempData[WellKnownTempData.SuccessMessage] = "Email server password reset"; } + if (useCustomSMTP) + { + if (model.Settings.From is not null && !MailboxAddressValidator.IsMailboxAddress(model.Settings.From)) + { + ModelState.AddModelError("Settings.From", "Invalid email"); + } + if (!ModelState.IsValid) + return View(model); + var storeBlob = store.GetStoreBlob(); + if (storeBlob.EmailSettings != null && new EmailsViewModel(storeBlob.EmailSettings, model.FallbackSettings).PasswordSet) + { + model.Settings.Password = storeBlob.EmailSettings.Password; + } + storeBlob.EmailSettings = model.Settings; + store.SetStoreBlob(storeBlob); + await _storeRepo.UpdateStore(store); + TempData[WellKnownTempData.SuccessMessage] = "Email settings modified"; + } + return RedirectToAction(nameof(StoreEmailSettings), new { storeId }); + } + + private static async Task IsSetupComplete(IEmailSender emailSender) + { + return emailSender is not null && (await emailSender.GetEmailSettings())?.IsComplete() == true; } } diff --git a/BTCPayServer/Controllers/UIStoresController.Integrations.cs b/BTCPayServer/Controllers/UIStoresController.Integrations.cs index f5582afd1..ec6bd3e43 100644 --- a/BTCPayServer/Controllers/UIStoresController.Integrations.cs +++ b/BTCPayServer/Controllers/UIStoresController.Integrations.cs @@ -13,22 +13,22 @@ using Microsoft.AspNetCore.Mvc; using NBitcoin; using NBitcoin.DataEncoders; -namespace BTCPayServer.Controllers -{ - public partial class UIStoresController - { - private async Task LastDeliveryForWebhook(string webhookId) - { - return (await _storeRepo.GetWebhookDeliveries(CurrentStore.Id, webhookId, 1)).ToList().FirstOrDefault(); - } +namespace BTCPayServer.Controllers; - [HttpGet("{storeId}/webhooks")] - public async Task Webhooks() +public partial class UIStoresController +{ + private async Task LastDeliveryForWebhook(string webhookId) + { + return (await _storeRepo.GetWebhookDeliveries(CurrentStore.Id, webhookId, 1)).ToList().FirstOrDefault(); + } + + [HttpGet("{storeId}/webhooks")] + public async Task Webhooks() + { + var webhooks = await _storeRepo.GetWebhooks(CurrentStore.Id); + return View(nameof(Webhooks), new WebhooksViewModel { - var webhooks = await _storeRepo.GetWebhooks(CurrentStore.Id); - return View(nameof(Webhooks), new WebhooksViewModel - { - Webhooks = webhooks.Select(async w => + Webhooks = webhooks.Select(async w => { var lastDelivery = await LastDeliveryForWebhook(w.Id); var lastDeliveryBlob = lastDelivery?.GetBlob(); @@ -42,150 +42,149 @@ namespace BTCPayServer.Controllers LastDeliverySuccessful = lastDeliveryBlob == null ? true : lastDeliveryBlob.Status == WebhookDeliveryStatus.HttpSuccess, }; } - ).Select(t => t.Result).ToArray() + ).Select(t => t.Result).ToArray() + }); + } + + [HttpGet("{storeId}/webhooks/new")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public IActionResult NewWebhook() + { + return View(nameof(ModifyWebhook), new EditWebhookViewModel + { + Active = true, + Everything = true, + IsNew = true, + Secret = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20)) + }); + } + + [HttpGet("{storeId}/webhooks/{webhookId}/remove")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task DeleteWebhook(string webhookId) + { + var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId); + if (webhook is null) + return NotFound(); + + return View("Confirm", new ConfirmModel("Delete webhook", "This webhook will be removed from this store. Are you sure?", "Delete")); + } + + [HttpPost("{storeId}/webhooks/{webhookId}/remove")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task DeleteWebhookPost(string webhookId) + { + var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId); + if (webhook is null) + return NotFound(); + + await _storeRepo.DeleteWebhook(CurrentStore.Id, webhookId); + TempData[WellKnownTempData.SuccessMessage] = "Webhook successfully deleted"; + return RedirectToAction(nameof(Webhooks), new { storeId = CurrentStore.Id }); + } + + [HttpPost("{storeId}/webhooks/new")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task NewWebhook(string storeId, EditWebhookViewModel viewModel) + { + if (!ModelState.IsValid) + return View(nameof(ModifyWebhook), viewModel); + + await _storeRepo.CreateWebhook(CurrentStore.Id, viewModel.CreateBlob()); + TempData[WellKnownTempData.SuccessMessage] = "The webhook has been created"; + return RedirectToAction(nameof(Webhooks), new { storeId }); + } + + [HttpGet("{storeId}/webhooks/{webhookId}")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task ModifyWebhook(string webhookId) + { + var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId); + if (webhook is null) + return NotFound(); + + var blob = webhook.GetBlob(); + var deliveries = await _storeRepo.GetWebhookDeliveries(CurrentStore.Id, webhookId, 20); + return View(nameof(ModifyWebhook), new EditWebhookViewModel(blob) + { + Deliveries = deliveries + .Select(s => new DeliveryViewModel(s)).ToList() + }); + } + + [HttpPost("{storeId}/webhooks/{webhookId}")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task ModifyWebhook(string webhookId, EditWebhookViewModel viewModel) + { + var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId); + if (webhook is null) + return NotFound(); + if (!ModelState.IsValid) + return View(nameof(ModifyWebhook), viewModel); + + await _storeRepo.UpdateWebhook(CurrentStore.Id, webhookId, viewModel.CreateBlob()); + TempData[WellKnownTempData.SuccessMessage] = "The webhook has been updated"; + return RedirectToAction(nameof(Webhooks), new { storeId = CurrentStore.Id }); + } + + [HttpGet("{storeId}/webhooks/{webhookId}/test")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task TestWebhook(string webhookId) + { + var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId); + if (webhook is null) + return NotFound(); + + return View(nameof(TestWebhook)); + } + + [HttpPost("{storeId}/webhooks/{webhookId}/test")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task TestWebhook(string webhookId, TestWebhookViewModel viewModel, CancellationToken cancellationToken) + { + var result = await _webhookNotificationManager.TestWebhook(CurrentStore.Id, webhookId, viewModel.Type, cancellationToken); + + if (result.Success) + { + TempData[WellKnownTempData.SuccessMessage] = $"{viewModel.Type} event delivered successfully! Delivery ID is {result.DeliveryId}"; + } + else + { + TempData[WellKnownTempData.ErrorMessage] = $"{viewModel.Type} event could not be delivered. Error message received: {(result.ErrorMessage ?? "unknown")}"; + } + + return View(nameof(TestWebhook)); + } + + [HttpPost("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task RedeliverWebhook(string webhookId, string deliveryId) + { + var delivery = await _storeRepo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId); + if (delivery is null) + return NotFound(); + + var newDeliveryId = await _webhookNotificationManager.Redeliver(deliveryId); + if (newDeliveryId is null) + return NotFound(); + + TempData[WellKnownTempData.SuccessMessage] = "Successfully planned a redelivery"; + return RedirectToAction(nameof(ModifyWebhook), + new + { + storeId = CurrentStore.Id, + webhookId }); - } + } - [HttpGet("{storeId}/webhooks/new")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public IActionResult NewWebhook() - { - return View(nameof(ModifyWebhook), new EditWebhookViewModel - { - Active = true, - Everything = true, - IsNew = true, - Secret = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20)) - }); - } + [HttpGet("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task WebhookDelivery(string webhookId, string deliveryId) + { + var delivery = await _storeRepo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId); + if (delivery is null) + return NotFound(); - [HttpGet("{storeId}/webhooks/{webhookId}/remove")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task DeleteWebhook(string webhookId) - { - var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId); - if (webhook is null) - return NotFound(); - - return View("Confirm", new ConfirmModel("Delete webhook", "This webhook will be removed from this store. Are you sure?", "Delete")); - } - - [HttpPost("{storeId}/webhooks/{webhookId}/remove")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task DeleteWebhookPost(string webhookId) - { - var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId); - if (webhook is null) - return NotFound(); - - await _storeRepo.DeleteWebhook(CurrentStore.Id, webhookId); - TempData[WellKnownTempData.SuccessMessage] = "Webhook successfully deleted"; - return RedirectToAction(nameof(Webhooks), new { storeId = CurrentStore.Id }); - } - - [HttpPost("{storeId}/webhooks/new")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task NewWebhook(string storeId, EditWebhookViewModel viewModel) - { - if (!ModelState.IsValid) - return View(nameof(ModifyWebhook), viewModel); - - await _storeRepo.CreateWebhook(CurrentStore.Id, viewModel.CreateBlob()); - TempData[WellKnownTempData.SuccessMessage] = "The webhook has been created"; - return RedirectToAction(nameof(Webhooks), new { storeId }); - } - - [HttpGet("{storeId}/webhooks/{webhookId}")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task ModifyWebhook(string webhookId) - { - var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId); - if (webhook is null) - return NotFound(); - - var blob = webhook.GetBlob(); - var deliveries = await _storeRepo.GetWebhookDeliveries(CurrentStore.Id, webhookId, 20); - return View(nameof(ModifyWebhook), new EditWebhookViewModel(blob) - { - Deliveries = deliveries - .Select(s => new DeliveryViewModel(s)).ToList() - }); - } - - [HttpPost("{storeId}/webhooks/{webhookId}")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task ModifyWebhook(string webhookId, EditWebhookViewModel viewModel) - { - var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId); - if (webhook is null) - return NotFound(); - if (!ModelState.IsValid) - return View(nameof(ModifyWebhook), viewModel); - - await _storeRepo.UpdateWebhook(CurrentStore.Id, webhookId, viewModel.CreateBlob()); - TempData[WellKnownTempData.SuccessMessage] = "The webhook has been updated"; - return RedirectToAction(nameof(Webhooks), new { storeId = CurrentStore.Id }); - } - - [HttpGet("{storeId}/webhooks/{webhookId}/test")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task TestWebhook(string webhookId) - { - var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId); - if (webhook is null) - return NotFound(); - - return View(nameof(TestWebhook)); - } - - [HttpPost("{storeId}/webhooks/{webhookId}/test")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task TestWebhook(string webhookId, TestWebhookViewModel viewModel, CancellationToken cancellationToken) - { - var result = await _webhookNotificationManager.TestWebhook(CurrentStore.Id, webhookId, viewModel.Type, cancellationToken); - - if (result.Success) - { - TempData[WellKnownTempData.SuccessMessage] = $"{viewModel.Type} event delivered successfully! Delivery ID is {result.DeliveryId}"; - } - else - { - TempData[WellKnownTempData.ErrorMessage] = $"{viewModel.Type} event could not be delivered. Error message received: {(result.ErrorMessage ?? "unknown")}"; - } - - return View(nameof(TestWebhook)); - } - - [HttpPost("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task RedeliverWebhook(string webhookId, string deliveryId) - { - var delivery = await _storeRepo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId); - if (delivery is null) - return NotFound(); - - var newDeliveryId = await _webhookNotificationManager.Redeliver(deliveryId); - if (newDeliveryId is null) - return NotFound(); - - TempData[WellKnownTempData.SuccessMessage] = "Successfully planned a redelivery"; - return RedirectToAction(nameof(ModifyWebhook), - new - { - storeId = CurrentStore.Id, - webhookId - }); - } - - [HttpGet("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task WebhookDelivery(string webhookId, string deliveryId) - { - var delivery = await _storeRepo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId); - if (delivery is null) - return NotFound(); - - return File(delivery.GetBlob().Request, "application/json"); - } + return File(delivery.GetBlob().Request, "application/json"); } } diff --git a/BTCPayServer/Controllers/UIStoresController.LightningLike.cs b/BTCPayServer/Controllers/UIStoresController.LightningLike.cs index a442f15a8..8c9d30018 100644 --- a/BTCPayServer/Controllers/UIStoresController.LightningLike.cs +++ b/BTCPayServer/Controllers/UIStoresController.LightningLike.cs @@ -16,328 +16,327 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json.Linq; -namespace BTCPayServer.Controllers +namespace BTCPayServer.Controllers; + +public partial class UIStoresController { - public partial class UIStoresController + [HttpGet("{storeId}/lightning/{cryptoCode}")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public IActionResult Lightning(string storeId, string cryptoCode) { - [HttpGet("{storeId}/lightning/{cryptoCode}")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public IActionResult Lightning(string storeId, string cryptoCode) + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + + var vm = new LightningViewModel { - var store = HttpContext.GetStoreData(); - if (store == null) - return NotFound(); + CryptoCode = cryptoCode, + StoreId = storeId + }; + SetExistingValues(store, vm); - var vm = new LightningViewModel - { - CryptoCode = cryptoCode, - StoreId = storeId - }; - SetExistingValues(store, vm); - - if (vm.LightningNodeType == LightningNodeType.Internal) - { - var services = _externalServiceOptions.Value.ExternalServices.ToList() - .Where(service => ExternalServices.LightningServiceTypes.Contains(service.Type)) - .Select(async service => - { - var model = new AdditionalServiceViewModel - { - DisplayName = service.DisplayName, - ServiceName = service.ServiceName, - CryptoCode = service.CryptoCode, - Type = service.Type.ToString() - }; - try - { - model.Link = await service.GetLink(Request.GetAbsoluteUriNoPathBase(), _btcpayServerOptions.NetworkType); - } - catch (Exception exception) - { - model.Error = exception.Message; - } - return model; - }) - .Select(t => t.Result) - .ToList(); - - // other services - foreach ((string key, Uri value) in _externalServiceOptions.Value.OtherExternalServices) + if (vm.LightningNodeType == LightningNodeType.Internal) + { + var services = _externalServiceOptions.Value.ExternalServices.ToList() + .Where(service => ExternalServices.LightningServiceTypes.Contains(service.Type)) + .Select(async service => { - if (ExternalServices.LightningServiceNames.Contains(key)) + var model = new AdditionalServiceViewModel { - services.Add(new AdditionalServiceViewModel - { - DisplayName = key, - ServiceName = key, - Type = key.Replace(" ", ""), - Link = Request.GetAbsoluteUriNoPathBase(value).AbsoluteUri - }); - } - } - - vm.Services = services; - } - - return View(vm); - } - - [HttpGet("{storeId}/lightning/{cryptoCode}/setup")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public IActionResult SetupLightningNode(string storeId, string cryptoCode) - { - var store = HttpContext.GetStoreData(); - if (store == null) - return NotFound(); - - var vm = new LightningNodeViewModel - { - CryptoCode = cryptoCode, - StoreId = storeId - }; - SetExistingValues(store, vm); - return View(vm); - } - - [HttpPost("{storeId}/lightning/{cryptoCode}/setup")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task SetupLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode) - { - vm.CryptoCode = cryptoCode; - var store = HttpContext.GetStoreData(); - if (store == null) - return NotFound(); - - var network = _explorerProvider.GetNetwork(vm.CryptoCode); - var oldConf = _handlers.GetLightningConfig(store, network); - - vm.CanUseInternalNode = CanUseInternalLightning(vm.CryptoCode); - - if (vm.CryptoCode == null) - { - ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network"); - return View(vm); - } - - - var paymentMethodId = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode); - - LightningPaymentMethodConfig? paymentMethod; - if (vm.LightningNodeType == LightningNodeType.Internal) - { - paymentMethod = new LightningPaymentMethodConfig(); - paymentMethod.SetInternalNode(); - } - else - { - if (string.IsNullOrEmpty(vm.ConnectionString)) - { - ModelState.AddModelError(nameof(vm.ConnectionString), "Please provide a connection string"); - return View(vm); - } - paymentMethod = new LightningPaymentMethodConfig { ConnectionString = vm.ConnectionString }; - } - - var handler = (LightningLikePaymentHandler)_handlers[paymentMethodId]; - var ctx = new PaymentMethodConfigValidationContext(_authorizationService, ModelState, - JToken.FromObject(paymentMethod, handler.Serializer), User, oldConf is null ? null : JToken.FromObject(oldConf, handler.Serializer)); - await handler.ValidatePaymentMethodConfig(ctx); - if (ctx.MissingPermission is not null) - ModelState.AddModelError(nameof(vm.ConnectionString), "You do not have the permissions to change this settings"); - if (!ModelState.IsValid) - return View(vm); - - switch (command) - { - case "save": - var lnurl = PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode); - store.SetPaymentMethodConfig(_handlers[paymentMethodId], paymentMethod); - store.SetPaymentMethodConfig(_handlers[lnurl], new LNURLPaymentMethodConfig - { - UseBech32Scheme = true, - LUD12Enabled = false - }); - - await _storeRepo.UpdateStore(store); - TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning node updated."; - return RedirectToAction(nameof(LightningSettings), new { storeId, cryptoCode }); - - case "test": + DisplayName = service.DisplayName, + ServiceName = service.ServiceName, + CryptoCode = service.CryptoCode, + Type = service.Type.ToString() + }; try { - var info = await handler.GetNodeInfo(paymentMethod, null, Request.IsOnion(), true); - var hasPublicAddress = info.Any(); - if (!vm.SkipPortTest && hasPublicAddress) - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); - await handler.TestConnection(info.First(), cts.Token); - } - TempData[WellKnownTempData.SuccessMessage] = "Connection to the Lightning node successful" + (hasPublicAddress - ? $". Your node address: {info.First()}" - : ", but no public address has been configured"); + model.Link = await service.GetLink(Request.GetAbsoluteUriNoPathBase(), _btcpayServerOptions.NetworkType); } - catch (Exception ex) + catch (Exception exception) { - TempData[WellKnownTempData.ErrorMessage] = ex.Message; - return View(vm); + model.Error = exception.Message; } - return View(vm); + return model; + }) + .Select(t => t.Result) + .ToList(); - default: - return View(vm); + // other services + foreach ((string key, Uri value) in _externalServiceOptions.Value.OtherExternalServices) + { + if (ExternalServices.LightningServiceNames.Contains(key)) + { + services.Add(new AdditionalServiceViewModel + { + DisplayName = key, + ServiceName = key, + Type = key.Replace(" ", ""), + Link = Request.GetAbsoluteUriNoPathBase(value).AbsoluteUri + }); + } } + + vm.Services = services; } - [HttpGet("{storeId}/lightning/{cryptoCode}/settings")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public IActionResult LightningSettings(string storeId, string cryptoCode) + return View(vm); + } + + [HttpGet("{storeId}/lightning/{cryptoCode}/setup")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public IActionResult SetupLightningNode(string storeId, string cryptoCode) + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + + var vm = new LightningNodeViewModel { - var store = HttpContext.GetStoreData(); - if (store == null) - return NotFound(); + CryptoCode = cryptoCode, + StoreId = storeId + }; + SetExistingValues(store, vm); + return View(vm); + } - var storeBlob = store.GetStoreBlob(); - var excludeFilters = storeBlob.GetExcludedPaymentMethods(); - var lnId = PaymentTypes.LN.GetPaymentMethodId(cryptoCode); - var lightning = GetConfig(lnId, store); - if (lightning == null) - { - TempData[WellKnownTempData.ErrorMessage] = "You need to connect to a Lightning node before adjusting its settings."; + [HttpPost("{storeId}/lightning/{cryptoCode}/setup")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task SetupLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode) + { + vm.CryptoCode = cryptoCode; + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); - return RedirectToAction(nameof(SetupLightningNode), new { storeId, cryptoCode }); - } + var network = _explorerProvider.GetNetwork(vm.CryptoCode); + var oldConf = _handlers.GetLightningConfig(store, network); - var vm = new LightningSettingsViewModel - { - CryptoCode = cryptoCode, - StoreId = storeId, - Enabled = !excludeFilters.Match(lnId), - LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate, - LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi, - LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints, - OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback - }; - SetExistingValues(store, vm); - - var lnurlId = PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode); - var lnurl = GetConfig(lnurlId, store); - if (lnurl != null) - { - vm.LNURLEnabled = !store.GetStoreBlob().GetExcludedPaymentMethods().Match(lnurlId); - vm.LNURLBech32Mode = lnurl.UseBech32Scheme; - vm.LUD12Enabled = lnurl.LUD12Enabled; - } + vm.CanUseInternalNode = CanUseInternalLightning(vm.CryptoCode); + if (vm.CryptoCode == null) + { + ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network"); return View(vm); } - [HttpPost("{storeId}/lightning/{cryptoCode}/settings")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task LightningSettings(LightningSettingsViewModel vm) - { - var store = HttpContext.GetStoreData(); - if (store == null) - return NotFound(); + + var paymentMethodId = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode); - if (vm.CryptoCode == null) + LightningPaymentMethodConfig? paymentMethod; + if (vm.LightningNodeType == LightningNodeType.Internal) + { + paymentMethod = new LightningPaymentMethodConfig(); + paymentMethod.SetInternalNode(); + } + else + { + if (string.IsNullOrEmpty(vm.ConnectionString)) { - ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network"); + ModelState.AddModelError(nameof(vm.ConnectionString), "Please provide a connection string"); return View(vm); } + paymentMethod = new LightningPaymentMethodConfig { ConnectionString = vm.ConnectionString }; + } - var network = _explorerProvider.GetNetwork(vm.CryptoCode); - var needUpdate = false; - var blob = store.GetStoreBlob(); - blob.LightningDescriptionTemplate = vm.LightningDescriptionTemplate ?? string.Empty; - blob.LightningAmountInSatoshi = vm.LightningAmountInSatoshi; - blob.LightningPrivateRouteHints = vm.LightningPrivateRouteHints; - blob.OnChainWithLnInvoiceFallback = vm.OnChainWithLnInvoiceFallback; - var lnurlId = PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode); - blob.SetExcluded(lnurlId, !vm.LNURLEnabled); + var handler = (LightningLikePaymentHandler)_handlers[paymentMethodId]; + var ctx = new PaymentMethodConfigValidationContext(_authorizationService, ModelState, + JToken.FromObject(paymentMethod, handler.Serializer), User, oldConf is null ? null : JToken.FromObject(oldConf, handler.Serializer)); + await handler.ValidatePaymentMethodConfig(ctx); + if (ctx.MissingPermission is not null) + ModelState.AddModelError(nameof(vm.ConnectionString), "You do not have the permissions to change this settings"); + if (!ModelState.IsValid) + return View(vm); - var lnurl = GetConfig(PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode), store); - if (lnurl is null || ( - lnurl.UseBech32Scheme != vm.LNURLBech32Mode || - lnurl.LUD12Enabled != vm.LUD12Enabled)) - { - needUpdate = true; - } + switch (command) + { + case "save": + var lnurl = PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode); + store.SetPaymentMethodConfig(_handlers[paymentMethodId], paymentMethod); + store.SetPaymentMethodConfig(_handlers[lnurl], new LNURLPaymentMethodConfig + { + UseBech32Scheme = true, + LUD12Enabled = false + }); - store.SetPaymentMethodConfig(_handlers[lnurlId], new LNURLPaymentMethodConfig - { - UseBech32Scheme = vm.LNURLBech32Mode, - LUD12Enabled = vm.LUD12Enabled - }); - - if (store.SetStoreBlob(blob)) - { - needUpdate = true; - } - - if (needUpdate) - { await _storeRepo.UpdateStore(store); + TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning node updated."; + return RedirectToAction(nameof(LightningSettings), new { storeId, cryptoCode }); - TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning settings successfully updated."; - } + case "test": + try + { + var info = await handler.GetNodeInfo(paymentMethod, null, Request.IsOnion(), true); + var hasPublicAddress = info.Any(); + if (!vm.SkipPortTest && hasPublicAddress) + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + await handler.TestConnection(info.First(), cts.Token); + } + TempData[WellKnownTempData.SuccessMessage] = "Connection to the Lightning node successful" + (hasPublicAddress + ? $". Your node address: {info.First()}" + : ", but no public address has been configured"); + } + catch (Exception ex) + { + TempData[WellKnownTempData.ErrorMessage] = ex.Message; + return View(vm); + } + return View(vm); - return RedirectToAction(nameof(LightningSettings), new { vm.StoreId, vm.CryptoCode }); - } - - [HttpPost("{storeId}/lightning/{cryptoCode}/status")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task SetLightningNodeEnabled(string storeId, string cryptoCode, bool enabled) - { - var store = HttpContext.GetStoreData(); - if (store == null) - return NotFound(); - - var network = _explorerProvider.GetNetwork(cryptoCode); - if (network == null) - return NotFound(); - - var lightning = GetConfig(PaymentTypes.LN.GetPaymentMethodId(cryptoCode), store); - if (lightning == null) - return NotFound(); - - var paymentMethodId = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode); - var storeBlob = store.GetStoreBlob(); - storeBlob.SetExcluded(paymentMethodId, !enabled); - if (!enabled) - { - storeBlob.SetExcluded(PaymentTypes.LNURL.GetPaymentMethodId(network.CryptoCode), true); - } - store.SetStoreBlob(storeBlob); - await _storeRepo.UpdateStore(store); - TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning payments are now {(enabled ? "enabled" : "disabled")} for this store."; - - return RedirectToAction(nameof(LightningSettings), new { storeId, cryptoCode }); - } - - private bool CanUseInternalLightning(string cryptoCode) - { - return _lightningNetworkOptions.InternalLightningByCryptoCode.ContainsKey(cryptoCode.ToUpperInvariant()) && (User.IsInRole(Roles.ServerAdmin) || _policiesSettings.AllowLightningInternalNodeForAll); - } - - private void SetExistingValues(StoreData store, LightningNodeViewModel vm) - { - vm.CanUseInternalNode = CanUseInternalLightning(vm.CryptoCode); - var lightning = GetConfig(PaymentTypes.LN.GetPaymentMethodId(vm.CryptoCode), store); - - if (lightning != null) - { - vm.LightningNodeType = lightning.IsInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom; - vm.ConnectionString = lightning.GetDisplayableConnectionString(); - } - else - { - vm.LightningNodeType = vm.CanUseInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom; - } - } - - private T? GetConfig(PaymentMethodId paymentMethodId, StoreData store) where T: class - { - return store.GetPaymentMethodConfig(paymentMethodId, _handlers); + default: + return View(vm); } } + + [HttpGet("{storeId}/lightning/{cryptoCode}/settings")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public IActionResult LightningSettings(string storeId, string cryptoCode) + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + + var storeBlob = store.GetStoreBlob(); + var excludeFilters = storeBlob.GetExcludedPaymentMethods(); + var lnId = PaymentTypes.LN.GetPaymentMethodId(cryptoCode); + var lightning = GetConfig(lnId, store); + if (lightning == null) + { + TempData[WellKnownTempData.ErrorMessage] = "You need to connect to a Lightning node before adjusting its settings."; + + return RedirectToAction(nameof(SetupLightningNode), new { storeId, cryptoCode }); + } + + var vm = new LightningSettingsViewModel + { + CryptoCode = cryptoCode, + StoreId = storeId, + Enabled = !excludeFilters.Match(lnId), + LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate, + LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi, + LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints, + OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback + }; + SetExistingValues(store, vm); + + var lnurlId = PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode); + var lnurl = GetConfig(lnurlId, store); + if (lnurl != null) + { + vm.LNURLEnabled = !store.GetStoreBlob().GetExcludedPaymentMethods().Match(lnurlId); + vm.LNURLBech32Mode = lnurl.UseBech32Scheme; + vm.LUD12Enabled = lnurl.LUD12Enabled; + } + + return View(vm); + } + + [HttpPost("{storeId}/lightning/{cryptoCode}/settings")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task LightningSettings(LightningSettingsViewModel vm) + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + + if (vm.CryptoCode == null) + { + ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network"); + return View(vm); + } + + var network = _explorerProvider.GetNetwork(vm.CryptoCode); + var needUpdate = false; + var blob = store.GetStoreBlob(); + blob.LightningDescriptionTemplate = vm.LightningDescriptionTemplate ?? string.Empty; + blob.LightningAmountInSatoshi = vm.LightningAmountInSatoshi; + blob.LightningPrivateRouteHints = vm.LightningPrivateRouteHints; + blob.OnChainWithLnInvoiceFallback = vm.OnChainWithLnInvoiceFallback; + var lnurlId = PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode); + blob.SetExcluded(lnurlId, !vm.LNURLEnabled); + + var lnurl = GetConfig(PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode), store); + if (lnurl is null || ( + lnurl.UseBech32Scheme != vm.LNURLBech32Mode || + lnurl.LUD12Enabled != vm.LUD12Enabled)) + { + needUpdate = true; + } + + store.SetPaymentMethodConfig(_handlers[lnurlId], new LNURLPaymentMethodConfig + { + UseBech32Scheme = vm.LNURLBech32Mode, + LUD12Enabled = vm.LUD12Enabled + }); + + if (store.SetStoreBlob(blob)) + { + needUpdate = true; + } + + if (needUpdate) + { + await _storeRepo.UpdateStore(store); + + TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning settings successfully updated."; + } + + return RedirectToAction(nameof(LightningSettings), new { vm.StoreId, vm.CryptoCode }); + } + + [HttpPost("{storeId}/lightning/{cryptoCode}/status")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task SetLightningNodeEnabled(string storeId, string cryptoCode, bool enabled) + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + + var network = _explorerProvider.GetNetwork(cryptoCode); + if (network == null) + return NotFound(); + + var lightning = GetConfig(PaymentTypes.LN.GetPaymentMethodId(cryptoCode), store); + if (lightning == null) + return NotFound(); + + var paymentMethodId = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode); + var storeBlob = store.GetStoreBlob(); + storeBlob.SetExcluded(paymentMethodId, !enabled); + if (!enabled) + { + storeBlob.SetExcluded(PaymentTypes.LNURL.GetPaymentMethodId(network.CryptoCode), true); + } + store.SetStoreBlob(storeBlob); + await _storeRepo.UpdateStore(store); + TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning payments are now {(enabled ? "enabled" : "disabled")} for this store."; + + return RedirectToAction(nameof(LightningSettings), new { storeId, cryptoCode }); + } + + private bool CanUseInternalLightning(string cryptoCode) + { + return _lightningNetworkOptions.InternalLightningByCryptoCode.ContainsKey(cryptoCode.ToUpperInvariant()) && (User.IsInRole(Roles.ServerAdmin) || _policiesSettings.AllowLightningInternalNodeForAll); + } + + private void SetExistingValues(StoreData store, LightningNodeViewModel vm) + { + vm.CanUseInternalNode = CanUseInternalLightning(vm.CryptoCode); + var lightning = GetConfig(PaymentTypes.LN.GetPaymentMethodId(vm.CryptoCode), store); + + if (lightning != null) + { + vm.LightningNodeType = lightning.IsInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom; + vm.ConnectionString = lightning.GetDisplayableConnectionString(); + } + else + { + vm.LightningNodeType = vm.CanUseInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom; + } + } + + private T? GetConfig(PaymentMethodId paymentMethodId, StoreData store) where T: class + { + return store.GetPaymentMethodConfig(paymentMethodId, _handlers); + } } diff --git a/BTCPayServer/Controllers/UIStoresController.Onchain.cs b/BTCPayServer/Controllers/UIStoresController.Onchain.cs index 17661d00e..3a2a8cdfa 100644 --- a/BTCPayServer/Controllers/UIStoresController.Onchain.cs +++ b/BTCPayServer/Controllers/UIStoresController.Onchain.cs @@ -24,842 +24,841 @@ using NBXplorer.DerivationStrategy; using NBXplorer.Models; using Newtonsoft.Json.Linq; -namespace BTCPayServer.Controllers +namespace BTCPayServer.Controllers; + +public partial class UIStoresController { - public partial class UIStoresController + [HttpGet("{storeId}/onchain/{cryptoCode}")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public ActionResult SetupWallet(WalletSetupViewModel vm) { - [HttpGet("{storeId}/onchain/{cryptoCode}")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public ActionResult SetupWallet(WalletSetupViewModel vm) + var checkResult = IsAvailable(vm.CryptoCode, out var store, out _); + if (checkResult != null) { - var checkResult = IsAvailable(vm.CryptoCode, out var store, out _); - if (checkResult != null) - { - return checkResult; - } - - var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store); - vm.DerivationScheme = derivation?.AccountDerivation.ToString(); - - return View(vm); + return checkResult; } - [HttpGet("{storeId}/onchain/{cryptoCode}/import/{method?}")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task ImportWallet(WalletSetupViewModel vm) + var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store); + vm.DerivationScheme = derivation?.AccountDerivation.ToString(); + + return View(vm); + } + + [HttpGet("{storeId}/onchain/{cryptoCode}/import/{method?}")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task ImportWallet(WalletSetupViewModel vm) + { + var checkResult = IsAvailable(vm.CryptoCode, out _, out var network); + if (checkResult != null) { - var checkResult = IsAvailable(vm.CryptoCode, out _, out var network); - if (checkResult != null) - { - return checkResult; - } - - var (hotWallet, rpcImport) = await CanUseHotWallet(); - vm.Network = network; - vm.CanUseHotWallet = hotWallet; - vm.CanUseRPCImport = rpcImport; - vm.SupportTaproot = network.NBitcoinNetwork.Consensus.SupportTaproot; - vm.SupportSegwit = network.NBitcoinNetwork.Consensus.SupportSegwit; - - if (vm.Method == null) - { - vm.Method = WalletSetupMethod.ImportOptions; - } - else if (vm.Method == WalletSetupMethod.Seed) - { - vm.SetupRequest = new WalletSetupRequest(); - } - - return View(vm.ViewName, vm); + return checkResult; } - [HttpPost("{storeId}/onchain/{cryptoCode}/modify")] - [HttpPost("{storeId}/onchain/{cryptoCode}/import/{method}")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task UpdateWallet(WalletSetupViewModel vm) + var (hotWallet, rpcImport) = await CanUseHotWallet(); + vm.Network = network; + vm.CanUseHotWallet = hotWallet; + vm.CanUseRPCImport = rpcImport; + vm.SupportTaproot = network.NBitcoinNetwork.Consensus.SupportTaproot; + vm.SupportSegwit = network.NBitcoinNetwork.Consensus.SupportSegwit; + + if (vm.Method == null) { - var checkResult = IsAvailable(vm.CryptoCode, out var store, out var network); - if (checkResult != null) - { - return checkResult; - } - - vm.Network = network; - DerivationSchemeSettings strategy = null; - PaymentMethodId paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode); - BitcoinLikePaymentHandler handler = (BitcoinLikePaymentHandler)_handlers[paymentMethodId]; - var wallet = _walletProvider.GetWallet(network); - if (wallet == null) - { - return NotFound(); - } - - if (vm.WalletFile != null) - { - string fileContent = null; - try - { - fileContent = await ReadAllText(vm.WalletFile); - } - catch - { - // ignored - } - - if (fileContent is null || !_onChainWalletParsers.TryParseWalletFile(fileContent, network, out strategy, out _)) - { - ModelState.AddModelError(nameof(vm.WalletFile), $"Import failed, make sure you import a compatible wallet format"); - return View(vm.ViewName, vm); - } - } - else if (!string.IsNullOrEmpty(vm.WalletFileContent)) - { - if (!_onChainWalletParsers.TryParseWalletFile(vm.WalletFileContent, network, out strategy, out var error)) - { - ModelState.AddModelError(nameof(vm.WalletFileContent), $"QR import failed: {error}"); - return View(vm.ViewName, vm); - } - } - else if (!string.IsNullOrEmpty(vm.DerivationScheme)) - { - try - { - strategy = ParseDerivationStrategy(vm.DerivationScheme, network); - strategy.Source = "ManualDerivationScheme"; - if (!string.IsNullOrEmpty(vm.AccountKey)) - { - var accountKey = new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork); - var accountSettings = - strategy.AccountKeySettings.FirstOrDefault(a => a.AccountKey == accountKey); - if (accountSettings != null) - { - accountSettings.AccountKeyPath = - vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath); - accountSettings.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint) - ? null - : new HDFingerprint(Encoders.Hex.DecodeData(vm.RootFingerprint)); - } - } - vm.DerivationScheme = strategy.AccountDerivation.ToString(); - ModelState.Remove(nameof(vm.DerivationScheme)); - } - catch (Exception ex) - { - ModelState.AddModelError(nameof(vm.DerivationScheme), $"Invalid wallet format: {ex.Message}"); - return View(vm.ViewName, vm); - } - } - else if (!string.IsNullOrEmpty(vm.Config)) - { - try - { - strategy = handler.ParsePaymentMethodConfig(JToken.Parse(UnprotectString(vm.Config))); - } - catch - { - ModelState.AddModelError(nameof(vm.Config), "Config file was not in the correct format"); - return View(vm.ViewName, vm); - } - } - - if (strategy is null) - { - ModelState.AddModelError(nameof(vm.DerivationScheme), "Please provide your extended public key"); - return View(vm.ViewName, vm); - } - - vm.Config = ProtectString(JToken.FromObject(strategy, handler.Serializer).ToString()); - ModelState.Remove(nameof(vm.Config)); - - var storeBlob = store.GetStoreBlob(); - if (vm.Confirmation) - { - try - { - await wallet.TrackAsync(strategy.AccountDerivation); - store.SetPaymentMethodConfig(_handlers[paymentMethodId], strategy); - storeBlob.SetExcluded(paymentMethodId, false); - storeBlob.PayJoinEnabled = strategy.IsHotWallet && !(vm.SetupRequest?.PayJoinEnabled is false); - store.SetStoreBlob(storeBlob); - } - catch - { - ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid derivation scheme"); - return View(vm.ViewName, vm); - } - await _storeRepo.UpdateStore(store); - _eventAggregator.Publish(new WalletChangedEvent { WalletId = new WalletId(vm.StoreId, vm.CryptoCode) }); - - TempData[WellKnownTempData.SuccessMessage] = $"Wallet settings for {network.CryptoCode} have been updated."; - - // This is success case when derivation scheme is added to the store - return RedirectToAction(nameof(WalletSettings), new { storeId = vm.StoreId, cryptoCode = vm.CryptoCode }); - } - return ConfirmAddresses(vm, strategy, network.NBXplorerNetwork); + vm.Method = WalletSetupMethod.ImportOptions; + } + else if (vm.Method == WalletSetupMethod.Seed) + { + vm.SetupRequest = new WalletSetupRequest(); } - private string ProtectString(string str) + return View(vm.ViewName, vm); + } + + [HttpPost("{storeId}/onchain/{cryptoCode}/modify")] + [HttpPost("{storeId}/onchain/{cryptoCode}/import/{method}")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task UpdateWallet(WalletSetupViewModel vm) + { + var checkResult = IsAvailable(vm.CryptoCode, out var store, out var network); + if (checkResult != null) { - return Convert.ToBase64String(_dataProtector.Protect(Encoding.UTF8.GetBytes(str))); - } - private string UnprotectString(string str) - { - return Encoding.UTF8.GetString(_dataProtector.Unprotect(Convert.FromBase64String(str))); + return checkResult; } - [HttpGet("{storeId}/onchain/{cryptoCode}/generate/{method?}")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task GenerateWallet(WalletSetupViewModel vm) + vm.Network = network; + DerivationSchemeSettings strategy = null; + PaymentMethodId paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode); + BitcoinLikePaymentHandler handler = (BitcoinLikePaymentHandler)_handlers[paymentMethodId]; + var wallet = _walletProvider.GetWallet(network); + if (wallet == null) { - var checkResult = IsAvailable(vm.CryptoCode, out _, out var network); - if (checkResult != null) - { - return checkResult; - } - - var isHotWallet = vm.Method == WalletSetupMethod.HotWallet; - var (hotWallet, rpcImport) = await CanUseHotWallet(); - if (isHotWallet && !hotWallet) - { - return NotFound(); - } - - vm.CanUseHotWallet = hotWallet; - vm.CanUseRPCImport = rpcImport; - vm.SupportTaproot = network.NBitcoinNetwork.Consensus.SupportTaproot; - vm.SupportSegwit = network.NBitcoinNetwork.Consensus.SupportSegwit; - vm.Network = network; - - if (vm.Method == null) - { - vm.Method = WalletSetupMethod.GenerateOptions; - } - else - { - var canUsePayJoin = hotWallet && isHotWallet && network.SupportPayJoin; - vm.SetupRequest = new WalletSetupRequest - { - SavePrivateKeys = isHotWallet, - CanUsePayJoin = canUsePayJoin, - PayJoinEnabled = canUsePayJoin - }; - } - - return View(vm.ViewName, vm); + return NotFound(); } - internal GenerateWalletResponse GenerateWalletResponse; - - [HttpPost("{storeId}/onchain/{cryptoCode}/generate/{method}")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task GenerateWallet(string storeId, string cryptoCode, WalletSetupMethod method, WalletSetupRequest request) + if (vm.WalletFile != null) { - var checkResult = IsAvailable(cryptoCode, out _, out var network); - if (checkResult != null) - { - return checkResult; - } - - var (hotWallet, rpcImport) = await CanUseHotWallet(); - if (!hotWallet && request.SavePrivateKeys || !rpcImport && request.ImportKeysToRPC) - { - return NotFound(); - } - var handler = _handlers.GetBitcoinHandler(cryptoCode); - var client = _explorerProvider.GetExplorerClient(cryptoCode); - var isImport = method == WalletSetupMethod.Seed; - var vm = new WalletSetupViewModel - { - StoreId = storeId, - CryptoCode = cryptoCode, - Method = method, - SetupRequest = request, - Confirmation = !isImport, - Network = network, - Source = isImport ? "SeedImported" : "NBXplorerGenerated", - IsHotWallet = isImport ? request.SavePrivateKeys : method == WalletSetupMethod.HotWallet, - DerivationSchemeFormat = "BTCPay", - CanUseHotWallet = hotWallet, - CanUseRPCImport = rpcImport, - SupportTaproot = network.NBitcoinNetwork.Consensus.SupportTaproot, - SupportSegwit = network.NBitcoinNetwork.Consensus.SupportSegwit - }; - - if (isImport && string.IsNullOrEmpty(request.ExistingMnemonic)) - { - ModelState.AddModelError(nameof(request.ExistingMnemonic), "Please provide your existing seed"); - return View(vm.ViewName, vm); - } - - GenerateWalletResponse response; + string fileContent = null; try { - response = await client.GenerateWalletAsync(request); - if (response == null) - { - throw new Exception("Node unavailable"); - } + fileContent = await ReadAllText(vm.WalletFile); } - catch (Exception e) + catch { - TempData.SetStatusMessageModel(new StatusMessageModel - { - Severity = StatusMessageModel.StatusSeverity.Error, - Html = $"There was an error generating your wallet: {e.Message}" - }); + // ignored + } + + if (fileContent is null || !_onChainWalletParsers.TryParseWalletFile(fileContent, network, out strategy, out _)) + { + ModelState.AddModelError(nameof(vm.WalletFile), $"Import failed, make sure you import a compatible wallet format"); return View(vm.ViewName, vm); } - - var derivationSchemeSettings = new DerivationSchemeSettings(response.DerivationScheme, network); - if (method == WalletSetupMethod.Seed) + } + else if (!string.IsNullOrEmpty(vm.WalletFileContent)) + { + if (!_onChainWalletParsers.TryParseWalletFile(vm.WalletFileContent, network, out strategy, out var error)) { - derivationSchemeSettings.Source = "ImportedSeed"; - derivationSchemeSettings.IsHotWallet = request.SavePrivateKeys; + ModelState.AddModelError(nameof(vm.WalletFileContent), $"QR import failed: {error}"); + return View(vm.ViewName, vm); } - else + } + else if (!string.IsNullOrEmpty(vm.DerivationScheme)) + { + try { - derivationSchemeSettings.Source = "NBXplorerGenerated"; - derivationSchemeSettings.IsHotWallet = method == WalletSetupMethod.HotWallet; - } - - var accountSettings = derivationSchemeSettings.GetSigningAccountKeySettings(); - accountSettings.AccountKeyPath = response.AccountKeyPath.KeyPath; - accountSettings.RootFingerprint = response.AccountKeyPath.MasterFingerprint; - derivationSchemeSettings.AccountOriginal = response.DerivationScheme.ToString(); - - // Set wallet properties from generate response - vm.RootFingerprint = response.AccountKeyPath.MasterFingerprint.ToString(); - vm.AccountKey = response.AccountHDKey.Neuter().ToWif(); - vm.KeyPath = response.AccountKeyPath.KeyPath.ToString(); - vm.Config = ProtectString(JToken.FromObject(derivationSchemeSettings, handler.Serializer).ToString()); - - var result = await UpdateWallet(vm); - - if (!ModelState.IsValid || result is not RedirectToActionResult) - return result; - - if (!isImport) - { - TempData.SetStatusMessageModel(new StatusMessageModel + strategy = ParseDerivationStrategy(vm.DerivationScheme, network); + strategy.Source = "ManualDerivationScheme"; + if (!string.IsNullOrEmpty(vm.AccountKey)) { - Severity = StatusMessageModel.StatusSeverity.Success, - Html = "Your wallet has been generated." - }); - var seedVm = new RecoverySeedBackupViewModel - { - CryptoCode = cryptoCode, - Mnemonic = response.Mnemonic, - Passphrase = response.Passphrase, - IsStored = request.SavePrivateKeys, - ReturnUrl = Url.Action(nameof(GenerateWalletConfirm), new { storeId, cryptoCode }) - }; - if (_btcPayEnv.IsDeveloping) - { - GenerateWalletResponse = response; + var accountKey = new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork); + var accountSettings = + strategy.AccountKeySettings.FirstOrDefault(a => a.AccountKey == accountKey); + if (accountSettings != null) + { + accountSettings.AccountKeyPath = + vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath); + accountSettings.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint) + ? null + : new HDFingerprint(Encoders.Hex.DecodeData(vm.RootFingerprint)); + } } - return this.RedirectToRecoverySeedBackup(seedVm); + vm.DerivationScheme = strategy.AccountDerivation.ToString(); + ModelState.Remove(nameof(vm.DerivationScheme)); } - - TempData.SetStatusMessageModel(new StatusMessageModel + catch (Exception ex) { - Severity = StatusMessageModel.StatusSeverity.Warning, - Html = "Please check your addresses and confirm." - }); - return result; + ModelState.AddModelError(nameof(vm.DerivationScheme), $"Invalid wallet format: {ex.Message}"); + return View(vm.ViewName, vm); + } + } + else if (!string.IsNullOrEmpty(vm.Config)) + { + try + { + strategy = handler.ParsePaymentMethodConfig(JToken.Parse(UnprotectString(vm.Config))); + } + catch + { + ModelState.AddModelError(nameof(vm.Config), "Config file was not in the correct format"); + return View(vm.ViewName, vm); + } } - // The purpose of this action is to show the user a success message, which confirms - // that the store settings have been updated after generating a new wallet. - [HttpGet("{storeId}/onchain/{cryptoCode}/generate/confirm")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public ActionResult GenerateWalletConfirm(string storeId, string cryptoCode) + if (strategy is null) { - var checkResult = IsAvailable(cryptoCode, out _, out var network); - if (checkResult != null) + ModelState.AddModelError(nameof(vm.DerivationScheme), "Please provide your extended public key"); + return View(vm.ViewName, vm); + } + + vm.Config = ProtectString(JToken.FromObject(strategy, handler.Serializer).ToString()); + ModelState.Remove(nameof(vm.Config)); + + var storeBlob = store.GetStoreBlob(); + if (vm.Confirmation) + { + try { - return checkResult; + await wallet.TrackAsync(strategy.AccountDerivation); + store.SetPaymentMethodConfig(_handlers[paymentMethodId], strategy); + storeBlob.SetExcluded(paymentMethodId, false); + storeBlob.PayJoinEnabled = strategy.IsHotWallet && !(vm.SetupRequest?.PayJoinEnabled is false); + store.SetStoreBlob(storeBlob); } + catch + { + ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid derivation scheme"); + return View(vm.ViewName, vm); + } + await _storeRepo.UpdateStore(store); + _eventAggregator.Publish(new WalletChangedEvent { WalletId = new WalletId(vm.StoreId, vm.CryptoCode) }); TempData[WellKnownTempData.SuccessMessage] = $"Wallet settings for {network.CryptoCode} have been updated."; - var walletId = new WalletId(storeId, cryptoCode); - return RedirectToAction(nameof(UIWalletsController.WalletTransactions), "UIWallets", new { walletId }); + // This is success case when derivation scheme is added to the store + return RedirectToAction(nameof(WalletSettings), new { storeId = vm.StoreId, cryptoCode = vm.CryptoCode }); + } + return ConfirmAddresses(vm, strategy, network.NBXplorerNetwork); + } + + private string ProtectString(string str) + { + return Convert.ToBase64String(_dataProtector.Protect(Encoding.UTF8.GetBytes(str))); + } + private string UnprotectString(string str) + { + return Encoding.UTF8.GetString(_dataProtector.Unprotect(Convert.FromBase64String(str))); + } + + [HttpGet("{storeId}/onchain/{cryptoCode}/generate/{method?}")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task GenerateWallet(WalletSetupViewModel vm) + { + var checkResult = IsAvailable(vm.CryptoCode, out _, out var network); + if (checkResult != null) + { + return checkResult; } - [HttpGet("{storeId}/onchain/{cryptoCode}/settings")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task WalletSettings(string storeId, string cryptoCode) + var isHotWallet = vm.Method == WalletSetupMethod.HotWallet; + var (hotWallet, rpcImport) = await CanUseHotWallet(); + if (isHotWallet && !hotWallet) { - var checkResult = IsAvailable(cryptoCode, out var store, out var network); - if (checkResult != null) + return NotFound(); + } + + vm.CanUseHotWallet = hotWallet; + vm.CanUseRPCImport = rpcImport; + vm.SupportTaproot = network.NBitcoinNetwork.Consensus.SupportTaproot; + vm.SupportSegwit = network.NBitcoinNetwork.Consensus.SupportSegwit; + vm.Network = network; + + if (vm.Method == null) + { + vm.Method = WalletSetupMethod.GenerateOptions; + } + else + { + var canUsePayJoin = hotWallet && isHotWallet && network.SupportPayJoin; + vm.SetupRequest = new WalletSetupRequest { - return checkResult; - } - - var derivation = GetExistingDerivationStrategy(cryptoCode, store); - if (derivation == null) - { - return NotFound(); - } - - var storeBlob = store.GetStoreBlob(); - var excludeFilters = storeBlob.GetExcludedPaymentMethods(); - (bool canUseHotWallet, bool rpcImport) = await CanUseHotWallet(); - var client = _explorerProvider.GetExplorerClient(network); - - var handler = _handlers.GetBitcoinHandler(cryptoCode); - var vm = new WalletSettingsViewModel - { - StoreId = storeId, - CryptoCode = cryptoCode, - WalletId = new WalletId(storeId, cryptoCode), - Enabled = !excludeFilters.Match(handler.PaymentMethodId), - Network = network, - IsHotWallet = derivation.IsHotWallet, - Source = derivation.Source, - RootFingerprint = derivation.GetSigningAccountKeySettings().RootFingerprint.ToString(), - DerivationScheme = derivation.AccountDerivation.ToString(), - DerivationSchemeInput = derivation.AccountOriginal, - KeyPath = derivation.GetSigningAccountKeySettings().AccountKeyPath?.ToString(), - UriScheme = network.NBitcoinNetwork.UriScheme, - Label = derivation.Label, - SelectedSigningKey = derivation.SigningKey.ToString(), - NBXSeedAvailable = derivation.IsHotWallet && - canUseHotWallet && - !string.IsNullOrEmpty(await client.GetMetadataAsync(derivation.AccountDerivation, - WellknownMetadataKeys.MasterHDKey)), - AccountKeys = derivation.AccountKeySettings - .Select(e => new WalletSettingsAccountKeyViewModel - { - AccountKey = e.AccountKey.ToString(), - MasterFingerprint = e.RootFingerprint is { } fp ? fp.ToString() : null, - AccountKeyPath = e.AccountKeyPath == null ? "" : $"m/{e.AccountKeyPath}" - }).ToList(), - Config = ProtectString(JToken.FromObject(derivation, handler.Serializer).ToString()), - PayJoinEnabled = storeBlob.PayJoinEnabled, - MonitoringExpiration = (int)storeBlob.MonitoringExpiration.TotalMinutes, - SpeedPolicy = store.SpeedPolicy, - ShowRecommendedFee = storeBlob.ShowRecommendedFee, - RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget, - CanUseHotWallet = canUseHotWallet, - CanUseRPCImport = rpcImport, - CanUsePayJoin = canUseHotWallet && network.SupportPayJoin && derivation.IsHotWallet, - StoreName = store.StoreName, - + SavePrivateKeys = isHotWallet, + CanUsePayJoin = canUsePayJoin, + PayJoinEnabled = canUsePayJoin }; - - ViewData["ReplaceDescription"] = WalletReplaceWarning(derivation.IsHotWallet); - ViewData["RemoveDescription"] = WalletRemoveWarning(derivation.IsHotWallet, network.CryptoCode); - - return View(vm); } - [HttpPost("{storeId}/onchain/{cryptoCode}/settings/wallet")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task UpdateWalletSettings(WalletSettingsViewModel vm) + return View(vm.ViewName, vm); + } + + internal GenerateWalletResponse GenerateWalletResponse; + + [HttpPost("{storeId}/onchain/{cryptoCode}/generate/{method}")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task GenerateWallet(string storeId, string cryptoCode, WalletSetupMethod method, WalletSetupRequest request) + { + var checkResult = IsAvailable(cryptoCode, out _, out var network); + if (checkResult != null) { - var checkResult = IsAvailable(vm.CryptoCode, out var store, out var network); - if (checkResult != null) - { - return checkResult; - } - - var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store); - if (derivation == null) - { - return NotFound(); - } - var handler = _handlers.GetBitcoinHandler(vm.CryptoCode); - var storeBlob = store.GetStoreBlob(); - var excludeFilters = storeBlob.GetExcludedPaymentMethods(); - var currentlyEnabled = !excludeFilters.Match(handler.PaymentMethodId); - bool enabledChanged = currentlyEnabled != vm.Enabled; - bool needUpdate = enabledChanged; - string errorMessage = null; - - if (enabledChanged) - { - storeBlob.SetExcluded(handler.PaymentMethodId, !vm.Enabled); - store.SetStoreBlob(storeBlob); - } - - if (derivation.Label != vm.Label) - { - needUpdate = true; - derivation.Label = vm.Label; - } - - var signingKey = string.IsNullOrEmpty(vm.SelectedSigningKey) - ? null - : new BitcoinExtPubKey(vm.SelectedSigningKey, network.NBitcoinNetwork); - if (derivation.SigningKey != signingKey && signingKey != null) - { - needUpdate = true; - derivation.SigningKey = signingKey; - } - - for (int i = 0; i < derivation.AccountKeySettings.Length; i++) - { - KeyPath accountKeyPath; - HDFingerprint? rootFingerprint; - - try - { - accountKeyPath = string.IsNullOrWhiteSpace(vm.AccountKeys[i].AccountKeyPath) - ? null - : new KeyPath(vm.AccountKeys[i].AccountKeyPath); - - if (accountKeyPath != null && derivation.AccountKeySettings[i].AccountKeyPath != accountKeyPath) - { - needUpdate = true; - derivation.AccountKeySettings[i].AccountKeyPath = accountKeyPath; - } - } - catch (Exception ex) - { - errorMessage = $"{ex.Message}: {vm.AccountKeys[i].AccountKeyPath}"; - } - - try - { - rootFingerprint = string.IsNullOrWhiteSpace(vm.AccountKeys[i].MasterFingerprint) - ? null - : new HDFingerprint(Encoders.Hex.DecodeData(vm.AccountKeys[i].MasterFingerprint)); - - if (rootFingerprint != null && derivation.AccountKeySettings[i].RootFingerprint != rootFingerprint) - { - needUpdate = true; - derivation.AccountKeySettings[i].RootFingerprint = rootFingerprint; - } - } - catch (Exception ex) - { - errorMessage = $"{ex.Message}: {vm.AccountKeys[i].MasterFingerprint}"; - } - } - - if (store.SpeedPolicy != vm.SpeedPolicy) - { - store.SpeedPolicy = vm.SpeedPolicy; - needUpdate = true; - } - - if (needUpdate) - { - store.SetPaymentMethodConfig(handler, derivation); - - await _storeRepo.UpdateStore(store); - - if (string.IsNullOrEmpty(errorMessage)) - { - var successMessage = "Wallet settings successfully updated."; - if (enabledChanged) - { - _eventAggregator.Publish(new WalletChangedEvent { WalletId = new WalletId(vm.StoreId, vm.CryptoCode) }); - successMessage += $" {vm.CryptoCode} on-chain payments are now {(vm.Enabled ? "enabled" : "disabled")} for this store."; - } - - TempData[WellKnownTempData.SuccessMessage] = successMessage; - } - else - { - TempData[WellKnownTempData.ErrorMessage] = errorMessage; - } - } - - return RedirectToAction(nameof(WalletSettings), new { vm.StoreId, vm.CryptoCode }); + return checkResult; } - [HttpPost("{storeId}/onchain/{cryptoCode}/settings/payment")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task UpdatePaymentSettings(WalletSettingsViewModel vm) + var (hotWallet, rpcImport) = await CanUseHotWallet(); + if (!hotWallet && request.SavePrivateKeys || !rpcImport && request.ImportKeysToRPC) { - var checkResult = IsAvailable(vm.CryptoCode, out var store, out var network); - if (checkResult != null) - { - return checkResult; - } + return NotFound(); + } + var handler = _handlers.GetBitcoinHandler(cryptoCode); + var client = _explorerProvider.GetExplorerClient(cryptoCode); + var isImport = method == WalletSetupMethod.Seed; + var vm = new WalletSetupViewModel + { + StoreId = storeId, + CryptoCode = cryptoCode, + Method = method, + SetupRequest = request, + Confirmation = !isImport, + Network = network, + Source = isImport ? "SeedImported" : "NBXplorerGenerated", + IsHotWallet = isImport ? request.SavePrivateKeys : method == WalletSetupMethod.HotWallet, + DerivationSchemeFormat = "BTCPay", + CanUseHotWallet = hotWallet, + CanUseRPCImport = rpcImport, + SupportTaproot = network.NBitcoinNetwork.Consensus.SupportTaproot, + SupportSegwit = network.NBitcoinNetwork.Consensus.SupportSegwit + }; - var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store); - if (derivation == null) - { - return NotFound(); - } - - bool needUpdate = false; - var blob = store.GetStoreBlob(); - var payjoinChanged = blob.PayJoinEnabled != vm.PayJoinEnabled; - blob.MonitoringExpiration = TimeSpan.FromMinutes(vm.MonitoringExpiration); - blob.ShowRecommendedFee = vm.ShowRecommendedFee; - blob.RecommendedFeeBlockTarget = vm.RecommendedFeeBlockTarget; - blob.PayJoinEnabled = vm.PayJoinEnabled; - - if (store.SetStoreBlob(blob)) - { - needUpdate = true; - } - - if (needUpdate) - { - await _storeRepo.UpdateStore(store); - - TempData[WellKnownTempData.SuccessMessage] = "Payment settings successfully updated"; - - if (payjoinChanged && blob.PayJoinEnabled && network.SupportPayJoin) - { - var config = store.GetPaymentMethodConfig(PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode), _handlers); - if (config?.IsHotWallet is not true) - { - TempData.Remove(WellKnownTempData.SuccessMessage); - TempData.SetStatusMessageModel(new StatusMessageModel - { - Severity = StatusMessageModel.StatusSeverity.Warning, - Html = "The payment settings were updated successfully. However, PayJoin will not work, as this isn't a hot wallet." - }); - } - } - } - - return RedirectToAction(nameof(WalletSettings), new { vm.StoreId, vm.CryptoCode }); + if (isImport && string.IsNullOrEmpty(request.ExistingMnemonic)) + { + ModelState.AddModelError(nameof(request.ExistingMnemonic), "Please provide your existing seed"); + return View(vm.ViewName, vm); } - [HttpGet("{storeId}/onchain/{cryptoCode}/seed")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task WalletSeed(string storeId, string cryptoCode, CancellationToken cancellationToken = default) + GenerateWalletResponse response; + try { - var checkResult = IsAvailable(cryptoCode, out var store, out var network); - if (checkResult != null) + response = await client.GenerateWalletAsync(request); + if (response == null) { - return checkResult; + throw new Exception("Node unavailable"); } - - var derivation = GetExistingDerivationStrategy(cryptoCode, store); - if (derivation == null) - { - return NotFound(); - } - - (bool canUseHotWallet, bool _) = await CanUseHotWallet(); - if (!canUseHotWallet) - { - return NotFound(); - } - - var client = _explorerProvider.GetExplorerClient(network); - if (await GetSeed(client, derivation) != null) - { - var mnemonic = await client.GetMetadataAsync(derivation.AccountDerivation, - WellknownMetadataKeys.Mnemonic, cancellationToken); - var recoveryVm = new RecoverySeedBackupViewModel - { - CryptoCode = cryptoCode, - Mnemonic = mnemonic, - IsStored = true, - RequireConfirm = false, - ReturnUrl = Url.Action(nameof(WalletSettings), new { storeId, cryptoCode }) - }; - return this.RedirectToRecoverySeedBackup(recoveryVm); - } - + } + catch (Exception e) + { TempData.SetStatusMessageModel(new StatusMessageModel { Severity = StatusMessageModel.StatusSeverity.Error, - Message = "The seed was not found" + Html = $"There was an error generating your wallet: {e.Message}" }); - - return RedirectToAction(nameof(WalletSettings)); + return View(vm.ViewName, vm); } - [HttpGet("{storeId}/onchain/{cryptoCode}/replace")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public ActionResult ReplaceWallet(string storeId, string cryptoCode) + var derivationSchemeSettings = new DerivationSchemeSettings(response.DerivationScheme, network); + if (method == WalletSetupMethod.Seed) { - var checkResult = IsAvailable(cryptoCode, out var store, out var network); - if (checkResult != null) - { - return checkResult; - } + derivationSchemeSettings.Source = "ImportedSeed"; + derivationSchemeSettings.IsHotWallet = request.SavePrivateKeys; + } + else + { + derivationSchemeSettings.Source = "NBXplorerGenerated"; + derivationSchemeSettings.IsHotWallet = method == WalletSetupMethod.HotWallet; + } - var derivation = GetExistingDerivationStrategy(cryptoCode, store); + var accountSettings = derivationSchemeSettings.GetSigningAccountKeySettings(); + accountSettings.AccountKeyPath = response.AccountKeyPath.KeyPath; + accountSettings.RootFingerprint = response.AccountKeyPath.MasterFingerprint; + derivationSchemeSettings.AccountOriginal = response.DerivationScheme.ToString(); - return View("Confirm", new ConfirmModel + // Set wallet properties from generate response + vm.RootFingerprint = response.AccountKeyPath.MasterFingerprint.ToString(); + vm.AccountKey = response.AccountHDKey.Neuter().ToWif(); + vm.KeyPath = response.AccountKeyPath.KeyPath.ToString(); + vm.Config = ProtectString(JToken.FromObject(derivationSchemeSettings, handler.Serializer).ToString()); + + var result = await UpdateWallet(vm); + + if (!ModelState.IsValid || result is not RedirectToActionResult) + return result; + + if (!isImport) + { + TempData.SetStatusMessageModel(new StatusMessageModel { - Title = $"Replace {network.CryptoCode} wallet", - Description = WalletReplaceWarning(derivation.IsHotWallet), - Action = "Setup new wallet" + Severity = StatusMessageModel.StatusSeverity.Success, + Html = "Your wallet has been generated." }); + var seedVm = new RecoverySeedBackupViewModel + { + CryptoCode = cryptoCode, + Mnemonic = response.Mnemonic, + Passphrase = response.Passphrase, + IsStored = request.SavePrivateKeys, + ReturnUrl = Url.Action(nameof(GenerateWalletConfirm), new { storeId, cryptoCode }) + }; + if (_btcPayEnv.IsDeveloping) + { + GenerateWalletResponse = response; + } + return this.RedirectToRecoverySeedBackup(seedVm); } - [HttpPost("{storeId}/onchain/{cryptoCode}/replace")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public IActionResult ConfirmReplaceWallet(string storeId, string cryptoCode) + TempData.SetStatusMessageModel(new StatusMessageModel { - var checkResult = IsAvailable(cryptoCode, out var store, out _); - if (checkResult != null) - { - return checkResult; - } + Severity = StatusMessageModel.StatusSeverity.Warning, + Html = "Please check your addresses and confirm." + }); + return result; + } - var derivation = GetExistingDerivationStrategy(cryptoCode, store); - if (derivation == null) - { - return NotFound(); - } - - return RedirectToAction(nameof(SetupWallet), new { storeId, cryptoCode }); + // The purpose of this action is to show the user a success message, which confirms + // that the store settings have been updated after generating a new wallet. + [HttpGet("{storeId}/onchain/{cryptoCode}/generate/confirm")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public ActionResult GenerateWalletConfirm(string storeId, string cryptoCode) + { + var checkResult = IsAvailable(cryptoCode, out _, out var network); + if (checkResult != null) + { + return checkResult; } - [HttpGet("{storeId}/onchain/{cryptoCode}/delete")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public ActionResult DeleteWallet(string storeId, string cryptoCode) + TempData[WellKnownTempData.SuccessMessage] = $"Wallet settings for {network.CryptoCode} have been updated."; + + var walletId = new WalletId(storeId, cryptoCode); + return RedirectToAction(nameof(UIWalletsController.WalletTransactions), "UIWallets", new { walletId }); + } + + [HttpGet("{storeId}/onchain/{cryptoCode}/settings")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task WalletSettings(string storeId, string cryptoCode) + { + var checkResult = IsAvailable(cryptoCode, out var store, out var network); + if (checkResult != null) { - var checkResult = IsAvailable(cryptoCode, out var store, out var network); - if (checkResult != null) - { - return checkResult; - } - - var derivation = GetExistingDerivationStrategy(cryptoCode, store); - - return View("Confirm", new ConfirmModel - { - Title = $"Remove {network.CryptoCode} wallet", - Description = WalletRemoveWarning(derivation.IsHotWallet, network.CryptoCode), - Action = "Remove" - }); + return checkResult; } - [HttpPost("{storeId}/onchain/{cryptoCode}/delete")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task ConfirmDeleteWallet(string storeId, string cryptoCode) + var derivation = GetExistingDerivationStrategy(cryptoCode, store); + if (derivation == null) { - var checkResult = IsAvailable(cryptoCode, out var store, out var network); - if (checkResult != null) - { - return checkResult; - } - - var derivation = GetExistingDerivationStrategy(cryptoCode, store); - if (derivation == null) - { - return NotFound(); - } - - store.SetPaymentMethodConfig(PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode), null); - - await _storeRepo.UpdateStore(store); - _eventAggregator.Publish(new WalletChangedEvent { WalletId = new WalletId(storeId, cryptoCode) }); - - TempData[WellKnownTempData.SuccessMessage] = - $"On-Chain payment for {network.CryptoCode} has been removed."; - - return RedirectToAction(nameof(GeneralSettings), new { storeId }); + return NotFound(); } - private IActionResult ConfirmAddresses(WalletSetupViewModel vm, DerivationSchemeSettings strategy, NBXplorerNetwork network) + var storeBlob = store.GetStoreBlob(); + var excludeFilters = storeBlob.GetExcludedPaymentMethods(); + (bool canUseHotWallet, bool rpcImport) = await CanUseHotWallet(); + var client = _explorerProvider.GetExplorerClient(network); + + var handler = _handlers.GetBitcoinHandler(cryptoCode); + var vm = new WalletSettingsViewModel { - vm.DerivationScheme = strategy.AccountDerivation.ToString(); - var deposit = new KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit); - - if (!string.IsNullOrEmpty(vm.DerivationScheme)) - { - var line = strategy.AccountDerivation.GetLineFor(deposit); - - for (uint i = 0; i < 10; i++) + StoreId = storeId, + CryptoCode = cryptoCode, + WalletId = new WalletId(storeId, cryptoCode), + Enabled = !excludeFilters.Match(handler.PaymentMethodId), + Network = network, + IsHotWallet = derivation.IsHotWallet, + Source = derivation.Source, + RootFingerprint = derivation.GetSigningAccountKeySettings().RootFingerprint.ToString(), + DerivationScheme = derivation.AccountDerivation.ToString(), + DerivationSchemeInput = derivation.AccountOriginal, + KeyPath = derivation.GetSigningAccountKeySettings().AccountKeyPath?.ToString(), + UriScheme = network.NBitcoinNetwork.UriScheme, + Label = derivation.Label, + SelectedSigningKey = derivation.SigningKey.ToString(), + NBXSeedAvailable = derivation.IsHotWallet && + canUseHotWallet && + !string.IsNullOrEmpty(await client.GetMetadataAsync(derivation.AccountDerivation, + WellknownMetadataKeys.MasterHDKey)), + AccountKeys = derivation.AccountKeySettings + .Select(e => new WalletSettingsAccountKeyViewModel { - var keyPath = deposit.GetKeyPath(i); - var rootedKeyPath = vm.GetAccountKeypath()?.Derive(keyPath); - var derivation = line.Derive(i); - var address = network.CreateAddress(strategy.AccountDerivation, - line.KeyPathTemplate.GetKeyPath(i), - derivation.ScriptPubKey).ToString(); - vm.AddressSamples.Add((keyPath.ToString(), address, rootedKeyPath)); + AccountKey = e.AccountKey.ToString(), + MasterFingerprint = e.RootFingerprint is { } fp ? fp.ToString() : null, + AccountKeyPath = e.AccountKeyPath == null ? "" : $"m/{e.AccountKeyPath}" + }).ToList(), + Config = ProtectString(JToken.FromObject(derivation, handler.Serializer).ToString()), + PayJoinEnabled = storeBlob.PayJoinEnabled, + MonitoringExpiration = (int)storeBlob.MonitoringExpiration.TotalMinutes, + SpeedPolicy = store.SpeedPolicy, + ShowRecommendedFee = storeBlob.ShowRecommendedFee, + RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget, + CanUseHotWallet = canUseHotWallet, + CanUseRPCImport = rpcImport, + CanUsePayJoin = canUseHotWallet && network.SupportPayJoin && derivation.IsHotWallet, + StoreName = store.StoreName, + + }; + + ViewData["ReplaceDescription"] = WalletReplaceWarning(derivation.IsHotWallet); + ViewData["RemoveDescription"] = WalletRemoveWarning(derivation.IsHotWallet, network.CryptoCode); + + return View(vm); + } + + [HttpPost("{storeId}/onchain/{cryptoCode}/settings/wallet")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task UpdateWalletSettings(WalletSettingsViewModel vm) + { + var checkResult = IsAvailable(vm.CryptoCode, out var store, out var network); + if (checkResult != null) + { + return checkResult; + } + + var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store); + if (derivation == null) + { + return NotFound(); + } + var handler = _handlers.GetBitcoinHandler(vm.CryptoCode); + var storeBlob = store.GetStoreBlob(); + var excludeFilters = storeBlob.GetExcludedPaymentMethods(); + var currentlyEnabled = !excludeFilters.Match(handler.PaymentMethodId); + bool enabledChanged = currentlyEnabled != vm.Enabled; + bool needUpdate = enabledChanged; + string errorMessage = null; + + if (enabledChanged) + { + storeBlob.SetExcluded(handler.PaymentMethodId, !vm.Enabled); + store.SetStoreBlob(storeBlob); + } + + if (derivation.Label != vm.Label) + { + needUpdate = true; + derivation.Label = vm.Label; + } + + var signingKey = string.IsNullOrEmpty(vm.SelectedSigningKey) + ? null + : new BitcoinExtPubKey(vm.SelectedSigningKey, network.NBitcoinNetwork); + if (derivation.SigningKey != signingKey && signingKey != null) + { + needUpdate = true; + derivation.SigningKey = signingKey; + } + + for (int i = 0; i < derivation.AccountKeySettings.Length; i++) + { + KeyPath accountKeyPath; + HDFingerprint? rootFingerprint; + + try + { + accountKeyPath = string.IsNullOrWhiteSpace(vm.AccountKeys[i].AccountKeyPath) + ? null + : new KeyPath(vm.AccountKeys[i].AccountKeyPath); + + if (accountKeyPath != null && derivation.AccountKeySettings[i].AccountKeyPath != accountKeyPath) + { + needUpdate = true; + derivation.AccountKeySettings[i].AccountKeyPath = accountKeyPath; } } - - vm.Confirmation = true; - ModelState.Remove(nameof(vm.Config)); // Remove the cached value - - return View("ImportWallet/ConfirmAddresses", vm); - } - - private ActionResult IsAvailable(string cryptoCode, out StoreData store, out BTCPayNetwork network) - { - store = HttpContext.GetStoreData(); - network = cryptoCode == null ? null : _explorerProvider.GetNetwork(cryptoCode); - - return store == null || network == null ? NotFound() : null; - } - - private DerivationSchemeSettings GetExistingDerivationStrategy(string cryptoCode, StoreData store) - { - return store.GetPaymentMethodConfig(PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode), _handlers); - } - - private async Task GetSeed(ExplorerClient client, DerivationSchemeSettings derivation) - { - return derivation.IsHotWallet && - await client.GetMetadataAsync(derivation.AccountDerivation, WellknownMetadataKeys.MasterHDKey) is { } seed && - !string.IsNullOrEmpty(seed) ? seed : null; - } - - private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet() - { - return await _authorizationService.CanUseHotWallet(_policiesSettings, User); - } - - private async Task ReadAllText(IFormFile file) - { - using var stream = new StreamReader(file.OpenReadStream()); - return await stream.ReadToEndAsync(); - } - - private string WalletWarning(bool isHotWallet, string info) - { - var walletType = isHotWallet ? "hot" : "watch-only"; - var additionalText = isHotWallet - ? "" - : " or imported it into an external wallet. If you no longer have access to your private key (recovery seed), immediately replace the wallet"; - return - $"

Please note that this is a {_html.Encode(walletType)} wallet!

" + - $"

Do not proceed if you have not backed up the wallet{_html.Encode(additionalText)}.

" + - $"

This action will erase the current wallet data from the server. {_html.Encode(info)}

"; - } - - private string WalletReplaceWarning(bool isHotWallet) - { - return WalletWarning(isHotWallet, - "The current wallet will be replaced once you finish the setup of the new wallet. " + - "If you cancel the setup, the current wallet will stay active."); - } - - private string WalletRemoveWarning(bool isHotWallet, string cryptoCode) - { - return WalletWarning(isHotWallet, - $"The store won't be able to receive {cryptoCode} onchain payments until a new wallet is set up."); - } - - private DerivationSchemeSettings ParseDerivationStrategy(string derivationScheme, BTCPayNetwork network) - { - var parser = new DerivationSchemeParser(network); - var isOD = Regex.Match(derivationScheme, @"\(.*?\)"); - if (isOD.Success) + catch (Exception ex) { - var derivationSchemeSettings = new DerivationSchemeSettings(); - var result = parser.ParseOutputDescriptor(derivationScheme); - derivationSchemeSettings.AccountOriginal = derivationScheme.Trim(); - derivationSchemeSettings.AccountDerivation = result.Item1; - derivationSchemeSettings.AccountKeySettings = result.Item2?.Select((path, i) => new AccountKeySettings() - { - RootFingerprint = path?.MasterFingerprint, - AccountKeyPath = path?.KeyPath, - AccountKey = result.Item1.GetExtPubKeys().ElementAt(i).GetWif(parser.Network) - }).ToArray() ?? new AccountKeySettings[result.Item1.GetExtPubKeys().Count()]; - return derivationSchemeSettings; + errorMessage = $"{ex.Message}: {vm.AccountKeys[i].AccountKeyPath}"; } - var strategy = parser.Parse(derivationScheme); - return new DerivationSchemeSettings(strategy, network); + try + { + rootFingerprint = string.IsNullOrWhiteSpace(vm.AccountKeys[i].MasterFingerprint) + ? null + : new HDFingerprint(Encoders.Hex.DecodeData(vm.AccountKeys[i].MasterFingerprint)); + + if (rootFingerprint != null && derivation.AccountKeySettings[i].RootFingerprint != rootFingerprint) + { + needUpdate = true; + derivation.AccountKeySettings[i].RootFingerprint = rootFingerprint; + } + } + catch (Exception ex) + { + errorMessage = $"{ex.Message}: {vm.AccountKeys[i].MasterFingerprint}"; + } } + + if (store.SpeedPolicy != vm.SpeedPolicy) + { + store.SpeedPolicy = vm.SpeedPolicy; + needUpdate = true; + } + + if (needUpdate) + { + store.SetPaymentMethodConfig(handler, derivation); + + await _storeRepo.UpdateStore(store); + + if (string.IsNullOrEmpty(errorMessage)) + { + var successMessage = "Wallet settings successfully updated."; + if (enabledChanged) + { + _eventAggregator.Publish(new WalletChangedEvent { WalletId = new WalletId(vm.StoreId, vm.CryptoCode) }); + successMessage += $" {vm.CryptoCode} on-chain payments are now {(vm.Enabled ? "enabled" : "disabled")} for this store."; + } + + TempData[WellKnownTempData.SuccessMessage] = successMessage; + } + else + { + TempData[WellKnownTempData.ErrorMessage] = errorMessage; + } + } + + return RedirectToAction(nameof(WalletSettings), new { vm.StoreId, vm.CryptoCode }); + } + + [HttpPost("{storeId}/onchain/{cryptoCode}/settings/payment")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task UpdatePaymentSettings(WalletSettingsViewModel vm) + { + var checkResult = IsAvailable(vm.CryptoCode, out var store, out var network); + if (checkResult != null) + { + return checkResult; + } + + var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store); + if (derivation == null) + { + return NotFound(); + } + + bool needUpdate = false; + var blob = store.GetStoreBlob(); + var payjoinChanged = blob.PayJoinEnabled != vm.PayJoinEnabled; + blob.MonitoringExpiration = TimeSpan.FromMinutes(vm.MonitoringExpiration); + blob.ShowRecommendedFee = vm.ShowRecommendedFee; + blob.RecommendedFeeBlockTarget = vm.RecommendedFeeBlockTarget; + blob.PayJoinEnabled = vm.PayJoinEnabled; + + if (store.SetStoreBlob(blob)) + { + needUpdate = true; + } + + if (needUpdate) + { + await _storeRepo.UpdateStore(store); + + TempData[WellKnownTempData.SuccessMessage] = "Payment settings successfully updated"; + + if (payjoinChanged && blob.PayJoinEnabled && network.SupportPayJoin) + { + var config = store.GetPaymentMethodConfig(PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode), _handlers); + if (config?.IsHotWallet is not true) + { + TempData.Remove(WellKnownTempData.SuccessMessage); + TempData.SetStatusMessageModel(new StatusMessageModel + { + Severity = StatusMessageModel.StatusSeverity.Warning, + Html = "The payment settings were updated successfully. However, PayJoin will not work, as this isn't a hot wallet." + }); + } + } + } + + return RedirectToAction(nameof(WalletSettings), new { vm.StoreId, vm.CryptoCode }); + } + + [HttpGet("{storeId}/onchain/{cryptoCode}/seed")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task WalletSeed(string storeId, string cryptoCode, CancellationToken cancellationToken = default) + { + var checkResult = IsAvailable(cryptoCode, out var store, out var network); + if (checkResult != null) + { + return checkResult; + } + + var derivation = GetExistingDerivationStrategy(cryptoCode, store); + if (derivation == null) + { + return NotFound(); + } + + (bool canUseHotWallet, bool _) = await CanUseHotWallet(); + if (!canUseHotWallet) + { + return NotFound(); + } + + var client = _explorerProvider.GetExplorerClient(network); + if (await GetSeed(client, derivation) != null) + { + var mnemonic = await client.GetMetadataAsync(derivation.AccountDerivation, + WellknownMetadataKeys.Mnemonic, cancellationToken); + var recoveryVm = new RecoverySeedBackupViewModel + { + CryptoCode = cryptoCode, + Mnemonic = mnemonic, + IsStored = true, + RequireConfirm = false, + ReturnUrl = Url.Action(nameof(WalletSettings), new { storeId, cryptoCode }) + }; + return this.RedirectToRecoverySeedBackup(recoveryVm); + } + + TempData.SetStatusMessageModel(new StatusMessageModel + { + Severity = StatusMessageModel.StatusSeverity.Error, + Message = "The seed was not found" + }); + + return RedirectToAction(nameof(WalletSettings)); + } + + [HttpGet("{storeId}/onchain/{cryptoCode}/replace")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public ActionResult ReplaceWallet(string storeId, string cryptoCode) + { + var checkResult = IsAvailable(cryptoCode, out var store, out var network); + if (checkResult != null) + { + return checkResult; + } + + var derivation = GetExistingDerivationStrategy(cryptoCode, store); + + return View("Confirm", new ConfirmModel + { + Title = $"Replace {network.CryptoCode} wallet", + Description = WalletReplaceWarning(derivation.IsHotWallet), + Action = "Setup new wallet" + }); + } + + [HttpPost("{storeId}/onchain/{cryptoCode}/replace")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public IActionResult ConfirmReplaceWallet(string storeId, string cryptoCode) + { + var checkResult = IsAvailable(cryptoCode, out var store, out _); + if (checkResult != null) + { + return checkResult; + } + + var derivation = GetExistingDerivationStrategy(cryptoCode, store); + if (derivation == null) + { + return NotFound(); + } + + return RedirectToAction(nameof(SetupWallet), new { storeId, cryptoCode }); + } + + [HttpGet("{storeId}/onchain/{cryptoCode}/delete")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public ActionResult DeleteWallet(string storeId, string cryptoCode) + { + var checkResult = IsAvailable(cryptoCode, out var store, out var network); + if (checkResult != null) + { + return checkResult; + } + + var derivation = GetExistingDerivationStrategy(cryptoCode, store); + + return View("Confirm", new ConfirmModel + { + Title = $"Remove {network.CryptoCode} wallet", + Description = WalletRemoveWarning(derivation.IsHotWallet, network.CryptoCode), + Action = "Remove" + }); + } + + [HttpPost("{storeId}/onchain/{cryptoCode}/delete")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task ConfirmDeleteWallet(string storeId, string cryptoCode) + { + var checkResult = IsAvailable(cryptoCode, out var store, out var network); + if (checkResult != null) + { + return checkResult; + } + + var derivation = GetExistingDerivationStrategy(cryptoCode, store); + if (derivation == null) + { + return NotFound(); + } + + store.SetPaymentMethodConfig(PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode), null); + + await _storeRepo.UpdateStore(store); + _eventAggregator.Publish(new WalletChangedEvent { WalletId = new WalletId(storeId, cryptoCode) }); + + TempData[WellKnownTempData.SuccessMessage] = + $"On-Chain payment for {network.CryptoCode} has been removed."; + + return RedirectToAction(nameof(GeneralSettings), new { storeId }); + } + + private IActionResult ConfirmAddresses(WalletSetupViewModel vm, DerivationSchemeSettings strategy, NBXplorerNetwork network) + { + vm.DerivationScheme = strategy.AccountDerivation.ToString(); + var deposit = new KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit); + + if (!string.IsNullOrEmpty(vm.DerivationScheme)) + { + var line = strategy.AccountDerivation.GetLineFor(deposit); + + for (uint i = 0; i < 10; i++) + { + var keyPath = deposit.GetKeyPath(i); + var rootedKeyPath = vm.GetAccountKeypath()?.Derive(keyPath); + var derivation = line.Derive(i); + var address = network.CreateAddress(strategy.AccountDerivation, + line.KeyPathTemplate.GetKeyPath(i), + derivation.ScriptPubKey).ToString(); + vm.AddressSamples.Add((keyPath.ToString(), address, rootedKeyPath)); + } + } + + vm.Confirmation = true; + ModelState.Remove(nameof(vm.Config)); // Remove the cached value + + return View("ImportWallet/ConfirmAddresses", vm); + } + + private ActionResult IsAvailable(string cryptoCode, out StoreData store, out BTCPayNetwork network) + { + store = HttpContext.GetStoreData(); + network = cryptoCode == null ? null : _explorerProvider.GetNetwork(cryptoCode); + + return store == null || network == null ? NotFound() : null; + } + + private DerivationSchemeSettings GetExistingDerivationStrategy(string cryptoCode, StoreData store) + { + return store.GetPaymentMethodConfig(PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode), _handlers); + } + + private async Task GetSeed(ExplorerClient client, DerivationSchemeSettings derivation) + { + return derivation.IsHotWallet && + await client.GetMetadataAsync(derivation.AccountDerivation, WellknownMetadataKeys.MasterHDKey) is { } seed && + !string.IsNullOrEmpty(seed) ? seed : null; + } + + private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet() + { + return await _authorizationService.CanUseHotWallet(_policiesSettings, User); + } + + private async Task ReadAllText(IFormFile file) + { + using var stream = new StreamReader(file.OpenReadStream()); + return await stream.ReadToEndAsync(); + } + + private string WalletWarning(bool isHotWallet, string info) + { + var walletType = isHotWallet ? "hot" : "watch-only"; + var additionalText = isHotWallet + ? "" + : " or imported it into an external wallet. If you no longer have access to your private key (recovery seed), immediately replace the wallet"; + return + $"

Please note that this is a {_html.Encode(walletType)} wallet!

" + + $"

Do not proceed if you have not backed up the wallet{_html.Encode(additionalText)}.

" + + $"

This action will erase the current wallet data from the server. {_html.Encode(info)}

"; + } + + private string WalletReplaceWarning(bool isHotWallet) + { + return WalletWarning(isHotWallet, + "The current wallet will be replaced once you finish the setup of the new wallet. " + + "If you cancel the setup, the current wallet will stay active."); + } + + private string WalletRemoveWarning(bool isHotWallet, string cryptoCode) + { + return WalletWarning(isHotWallet, + $"The store won't be able to receive {cryptoCode} onchain payments until a new wallet is set up."); + } + + private DerivationSchemeSettings ParseDerivationStrategy(string derivationScheme, BTCPayNetwork network) + { + var parser = new DerivationSchemeParser(network); + var isOD = Regex.Match(derivationScheme, @"\(.*?\)"); + if (isOD.Success) + { + var derivationSchemeSettings = new DerivationSchemeSettings(); + var result = parser.ParseOutputDescriptor(derivationScheme); + derivationSchemeSettings.AccountOriginal = derivationScheme.Trim(); + derivationSchemeSettings.AccountDerivation = result.Item1; + derivationSchemeSettings.AccountKeySettings = result.Item2?.Select((path, i) => new AccountKeySettings() + { + RootFingerprint = path?.MasterFingerprint, + AccountKeyPath = path?.KeyPath, + AccountKey = result.Item1.GetExtPubKeys().ElementAt(i).GetWif(parser.Network) + }).ToArray() ?? new AccountKeySettings[result.Item1.GetExtPubKeys().Count()]; + return derivationSchemeSettings; + } + + var strategy = parser.Parse(derivationScheme); + return new DerivationSchemeSettings(strategy, network); } } diff --git a/BTCPayServer/Controllers/UIStoresController.Rates.cs b/BTCPayServer/Controllers/UIStoresController.Rates.cs index 237516bb1..c1b12cbf4 100644 --- a/BTCPayServer/Controllers/UIStoresController.Rates.cs +++ b/BTCPayServer/Controllers/UIStoresController.Rates.cs @@ -14,177 +14,176 @@ using BTCPayServer.Rating; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace BTCPayServer.Controllers +namespace BTCPayServer.Controllers; + +public partial class UIStoresController { - public partial class UIStoresController + [HttpGet("{storeId}/rates")] + public IActionResult Rates() { - [HttpGet("{storeId}/rates")] - public IActionResult Rates() + var exchanges = GetSupportedExchanges().ToList(); + var storeBlob = CurrentStore.GetStoreBlob(); + var vm = new RatesViewModel(); + vm.SetExchangeRates(exchanges, storeBlob.PreferredExchange ?? storeBlob.GetRecommendedExchange()); + vm.Spread = (double)(storeBlob.Spread * 100m); + vm.StoreId = CurrentStore.Id; + vm.Script = storeBlob.GetRateRules(_networkProvider).ToString(); + vm.DefaultScript = storeBlob.GetDefaultRateRules(_networkProvider).ToString(); + vm.AvailableExchanges = exchanges; + vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString(); + vm.ShowScripting = storeBlob.RateScripting; + return View(vm); + } + + [HttpPost("{storeId}/rates")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task Rates(RatesViewModel model, string? command = null, string? storeId = null, CancellationToken cancellationToken = default) + { + if (command == "scripting-on") { - var exchanges = GetSupportedExchanges().ToList(); - var storeBlob = CurrentStore.GetStoreBlob(); - var vm = new RatesViewModel(); - vm.SetExchangeRates(exchanges, storeBlob.PreferredExchange ?? storeBlob.GetRecommendedExchange()); - vm.Spread = (double)(storeBlob.Spread * 100m); - vm.StoreId = CurrentStore.Id; - vm.Script = storeBlob.GetRateRules(_networkProvider).ToString(); - vm.DefaultScript = storeBlob.GetDefaultRateRules(_networkProvider).ToString(); - vm.AvailableExchanges = exchanges; - vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString(); - vm.ShowScripting = storeBlob.RateScripting; - return View(vm); + return RedirectToAction(nameof(ShowRateRules), new { scripting = true, storeId = model.StoreId }); + } + if (command == "scripting-off") + { + return RedirectToAction(nameof(ShowRateRules), new { scripting = false, storeId = model.StoreId }); } - [HttpPost("{storeId}/rates")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task Rates(RatesViewModel model, string? command = null, string? storeId = null, CancellationToken cancellationToken = default) + var exchanges = GetSupportedExchanges().ToList(); + model.SetExchangeRates(exchanges, model.PreferredExchange ?? HttpContext.GetStoreData().GetStoreBlob().GetRecommendedExchange()); + model.StoreId = storeId ?? model.StoreId; + CurrencyPair[]? currencyPairs = null; + try { - if (command == "scripting-on") - { - return RedirectToAction(nameof(ShowRateRules), new { scripting = true, storeId = model.StoreId }); - } - if (command == "scripting-off") - { - return RedirectToAction(nameof(ShowRateRules), new { scripting = false, storeId = model.StoreId }); - } + currencyPairs = model.DefaultCurrencyPairs? + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(p => CurrencyPair.Parse(p)) + .ToArray(); + } + catch + { + ModelState.AddModelError(nameof(model.DefaultCurrencyPairs), "Invalid currency pairs (should be for example: BTC_USD,BTC_CAD,BTC_JPY)"); + } + if (!ModelState.IsValid) + { + return View(model); + } + if (model.PreferredExchange != null) + model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant(); - var exchanges = GetSupportedExchanges().ToList(); - model.SetExchangeRates(exchanges, model.PreferredExchange ?? HttpContext.GetStoreData().GetStoreBlob().GetRecommendedExchange()); - model.StoreId = storeId ?? model.StoreId; - CurrencyPair[]? currencyPairs = null; - try - { - currencyPairs = model.DefaultCurrencyPairs? - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(p => CurrencyPair.Parse(p)) - .ToArray(); - } - catch - { - ModelState.AddModelError(nameof(model.DefaultCurrencyPairs), "Invalid currency pairs (should be for example: BTC_USD,BTC_CAD,BTC_JPY)"); - } - if (!ModelState.IsValid) + var blob = CurrentStore.GetStoreBlob(); + model.DefaultScript = blob.GetDefaultRateRules(_networkProvider).ToString(); + model.AvailableExchanges = exchanges; + + blob.PreferredExchange = model.PreferredExchange; + blob.Spread = (decimal)model.Spread / 100.0m; + blob.DefaultCurrencyPairs = currencyPairs; + if (!model.ShowScripting) + { + if (!exchanges.Any(provider => provider.Id.Equals(model.PreferredExchange, StringComparison.InvariantCultureIgnoreCase))) { + ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})"); return View(model); } - if (model.PreferredExchange != null) - model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant(); - - var blob = CurrentStore.GetStoreBlob(); - model.DefaultScript = blob.GetDefaultRateRules(_networkProvider).ToString(); - model.AvailableExchanges = exchanges; - - blob.PreferredExchange = model.PreferredExchange; - blob.Spread = (decimal)model.Spread / 100.0m; - blob.DefaultCurrencyPairs = currencyPairs; - if (!model.ShowScripting) + } + RateRules? rules; + if (model.ShowScripting) + { + if (!RateRules.TryParse(model.Script, out rules, out var errors)) { - if (!exchanges.Any(provider => provider.Id.Equals(model.PreferredExchange, StringComparison.InvariantCultureIgnoreCase))) - { - ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})"); - return View(model); - } - } - RateRules? rules; - if (model.ShowScripting) - { - if (!RateRules.TryParse(model.Script, out rules, out var errors)) - { - errors ??= []; - var errorString = string.Join(", ", errors.ToArray()); - ModelState.AddModelError(nameof(model.Script), $"Parsing error ({errorString})"); - return View(model); - } - else - { - blob.RateScript = rules.ToString(); - ModelState.Remove(nameof(model.Script)); - model.Script = blob.RateScript; - } - } - rules = blob.GetRateRules(_networkProvider); - - if (command == "Test") - { - if (string.IsNullOrWhiteSpace(model.ScriptTest)) - { - ModelState.AddModelError(nameof(model.ScriptTest), "Fill out currency pair to test for (like BTC_USD,BTC_CAD)"); - return View(model); - } - var splitted = model.ScriptTest.Split(',', StringSplitOptions.RemoveEmptyEntries); - - var pairs = new List(); - foreach (var pair in splitted) - { - if (!CurrencyPair.TryParse(pair, out var currencyPair)) - { - ModelState.AddModelError(nameof(model.ScriptTest), $"Invalid currency pair '{pair}' (it should be formatted like BTC_USD,BTC_CAD)"); - return View(model); - } - pairs.Add(currencyPair); - } - - var fetchs = _rateFactory.FetchRates(pairs.ToHashSet(), rules, cancellationToken); - var testResults = new List(); - foreach (var fetch in fetchs) - { - var testResult = await (fetch.Value); - testResults.Add(new RatesViewModel.TestResultViewModel - { - CurrencyPair = fetch.Key.ToString(), - Error = testResult.Errors.Count != 0, - Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.BidAsk.Bid.ToString(CultureInfo.InvariantCulture) - : testResult.EvaluatedRule - }); - } - model.TestRateRules = testResults; + errors ??= []; + var errorString = string.Join(", ", errors.ToArray()); + ModelState.AddModelError(nameof(model.Script), $"Parsing error ({errorString})"); return View(model); } - - // command == Save - if (CurrentStore.SetStoreBlob(blob)) + else { - await _storeRepo.UpdateStore(CurrentStore); - TempData[WellKnownTempData.SuccessMessage] = "Rate settings updated"; + blob.RateScript = rules.ToString(); + ModelState.Remove(nameof(model.Script)); + model.Script = blob.RateScript; } - return RedirectToAction(nameof(Rates), new + } + rules = blob.GetRateRules(_networkProvider); + + if (command == "Test") + { + if (string.IsNullOrWhiteSpace(model.ScriptTest)) { - storeId = CurrentStore.Id - }); + ModelState.AddModelError(nameof(model.ScriptTest), "Fill out currency pair to test for (like BTC_USD,BTC_CAD)"); + return View(model); + } + var splitted = model.ScriptTest.Split(',', StringSplitOptions.RemoveEmptyEntries); + + var pairs = new List(); + foreach (var pair in splitted) + { + if (!CurrencyPair.TryParse(pair, out var currencyPair)) + { + ModelState.AddModelError(nameof(model.ScriptTest), $"Invalid currency pair '{pair}' (it should be formatted like BTC_USD,BTC_CAD)"); + return View(model); + } + pairs.Add(currencyPair); + } + + var fetchs = _rateFactory.FetchRates(pairs.ToHashSet(), rules, cancellationToken); + var testResults = new List(); + foreach (var fetch in fetchs) + { + var testResult = await (fetch.Value); + testResults.Add(new RatesViewModel.TestResultViewModel + { + CurrencyPair = fetch.Key.ToString(), + Error = testResult.Errors.Count != 0, + Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.BidAsk.Bid.ToString(CultureInfo.InvariantCulture) + : testResult.EvaluatedRule + }); + } + model.TestRateRules = testResults; + return View(model); } - [HttpGet("{storeId}/rates/confirm")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public IActionResult ShowRateRules(bool scripting) + // command == Save + if (CurrentStore.SetStoreBlob(blob)) { - return View("Confirm", new ConfirmModel - { - Action = "Continue", - Title = "Rate rule scripting", - Description = scripting ? - "This action will modify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)" - : "This action will delete your rate script. Are you sure to turn off rate rules scripting?", - ButtonClass = scripting ? "btn-primary" : "btn-danger" - }); - } - - [HttpPost("{storeId}/rates/confirm")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task ShowRateRulesPost(bool scripting) - { - var blob = CurrentStore.GetStoreBlob(); - blob.RateScripting = scripting; - blob.RateScript = blob.GetDefaultRateRules(_networkProvider).ToString(); - CurrentStore.SetStoreBlob(blob); await _storeRepo.UpdateStore(CurrentStore); - TempData[WellKnownTempData.SuccessMessage] = "Rate rules scripting " + (scripting ? "activated" : "deactivated"); - return RedirectToAction(nameof(Rates), new { storeId = CurrentStore.Id }); + TempData[WellKnownTempData.SuccessMessage] = "Rate settings updated"; } - - private IEnumerable GetSupportedExchanges() + return RedirectToAction(nameof(Rates), new { - return _rateFactory.RateProviderFactory.AvailableRateProviders - .OrderBy(s => s.DisplayName, StringComparer.OrdinalIgnoreCase); - } + storeId = CurrentStore.Id + }); + } + + [HttpGet("{storeId}/rates/confirm")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public IActionResult ShowRateRules(bool scripting) + { + return View("Confirm", new ConfirmModel + { + Action = "Continue", + Title = "Rate rule scripting", + Description = scripting ? + "This action will modify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)" + : "This action will delete your rate script. Are you sure to turn off rate rules scripting?", + ButtonClass = scripting ? "btn-primary" : "btn-danger" + }); + } + + [HttpPost("{storeId}/rates/confirm")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task ShowRateRulesPost(bool scripting) + { + var blob = CurrentStore.GetStoreBlob(); + blob.RateScripting = scripting; + blob.RateScript = blob.GetDefaultRateRules(_networkProvider).ToString(); + CurrentStore.SetStoreBlob(blob); + await _storeRepo.UpdateStore(CurrentStore); + TempData[WellKnownTempData.SuccessMessage] = "Rate rules scripting " + (scripting ? "activated" : "deactivated"); + return RedirectToAction(nameof(Rates), new { storeId = CurrentStore.Id }); + } + + private IEnumerable GetSupportedExchanges() + { + return _rateFactory.RateProviderFactory.AvailableRateProviders + .OrderBy(s => s.DisplayName, StringComparer.OrdinalIgnoreCase); } } diff --git a/BTCPayServer/Controllers/UIStoresController.Roles.cs b/BTCPayServer/Controllers/UIStoresController.Roles.cs index 4d20fbd78..b5bc9c34a 100644 --- a/BTCPayServer/Controllers/UIStoresController.Roles.cs +++ b/BTCPayServer/Controllers/UIStoresController.Roles.cs @@ -9,151 +9,150 @@ using BTCPayServer.Services.Stores; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace BTCPayServer.Controllers +namespace BTCPayServer.Controllers; + +public partial class UIStoresController { - public partial class UIStoresController + [HttpGet("{storeId}/roles")] + public async Task ListRoles( + string storeId, + [FromServices] StoreRepository storeRepository, + RolesViewModel model, + string sortOrder = null + ) { - [HttpGet("{storeId}/roles")] - public async Task ListRoles( - string storeId, - [FromServices] StoreRepository storeRepository, - RolesViewModel model, - string sortOrder = null - ) + var roles = await storeRepository.GetStoreRoles(storeId, true); + var defaultRole = (await storeRepository.GetDefaultRole()).Role; + model ??= new RolesViewModel(); + model.DefaultRole = defaultRole; + + switch (sortOrder) { - var roles = await storeRepository.GetStoreRoles(storeId, true); - var defaultRole = (await storeRepository.GetDefaultRole()).Role; - model ??= new RolesViewModel(); - model.DefaultRole = defaultRole; - - switch (sortOrder) - { - case "desc": - ViewData["NextRoleSortOrder"] = "asc"; - roles = roles.OrderByDescending(user => user.Role).ToArray(); - break; - case "asc": - roles = roles.OrderBy(user => user.Role).ToArray(); - ViewData["NextRoleSortOrder"] = "desc"; - break; - } - - model.Roles = roles.Skip(model.Skip).Take(model.Count).ToList(); - - return View(model); + case "desc": + ViewData["NextRoleSortOrder"] = "asc"; + roles = roles.OrderByDescending(user => user.Role).ToArray(); + break; + case "asc": + roles = roles.OrderBy(user => user.Role).ToArray(); + ViewData["NextRoleSortOrder"] = "desc"; + break; } - [HttpGet("{storeId}/roles/{role}")] - [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task CreateOrEditRole( - string storeId, - [FromServices] StoreRepository storeRepository, - string role) - { - if (role == "create") - { - ModelState.Remove(nameof(role)); - return View(new UpdateRoleViewModel()); - } + model.Roles = roles.Skip(model.Skip).Take(model.Count).ToList(); - var roleData = await storeRepository.GetStoreRole(new StoreRoleId(storeId, role)); - if (roleData == null) + return View(model); + } + + [HttpGet("{storeId}/roles/{role}")] + [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task CreateOrEditRole( + string storeId, + [FromServices] StoreRepository storeRepository, + string role) + { + if (role == "create") + { + ModelState.Remove(nameof(role)); + return View(new UpdateRoleViewModel()); + } + + var roleData = await storeRepository.GetStoreRole(new StoreRoleId(storeId, role)); + if (roleData == null) + return NotFound(); + return View(new UpdateRoleViewModel + { + Policies = roleData.Permissions, + Role = roleData.Role + }); + } + + [HttpPost("{storeId}/roles/{role}")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task CreateOrEditRole( + string storeId, + [FromServices] StoreRepository storeRepository, + [FromRoute] string role, UpdateRoleViewModel viewModel) + { + string successMessage; + StoreRoleId roleId; + if (role == "create") + { + successMessage = "Role created"; + role = viewModel.Role; + roleId = new StoreRoleId(storeId, role); + } + else + { + successMessage = "Role updated"; + roleId = new StoreRoleId(storeId, role); + var storeRole = await storeRepository.GetStoreRole(roleId); + if (storeRole == null) return NotFound(); - return View(new UpdateRoleViewModel - { - Policies = roleData.Permissions, - Role = roleData.Role - }); } - [HttpPost("{storeId}/roles/{role}")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task CreateOrEditRole( - string storeId, - [FromServices] StoreRepository storeRepository, - [FromRoute] string role, UpdateRoleViewModel viewModel) + if (!ModelState.IsValid) { - string successMessage; - StoreRoleId roleId; - if (role == "create") - { - successMessage = "Role created"; - role = viewModel.Role; - roleId = new StoreRoleId(storeId, role); - } - else - { - successMessage = "Role updated"; - roleId = new StoreRoleId(storeId, role); - var storeRole = await storeRepository.GetStoreRole(roleId); - if (storeRole == null) - return NotFound(); - } - - if (!ModelState.IsValid) - { - return View(viewModel); - } - - var r = await storeRepository.AddOrUpdateStoreRole(roleId, viewModel.Policies); - if (r is null) - { - TempData.SetStatusMessageModel(new StatusMessageModel() - { - Severity = StatusMessageModel.StatusSeverity.Error, - Message = "Role could not be updated" - }); - return View(viewModel); - } + return View(viewModel); + } + var r = await storeRepository.AddOrUpdateStoreRole(roleId, viewModel.Policies); + if (r is null) + { TempData.SetStatusMessageModel(new StatusMessageModel() { - Severity = StatusMessageModel.StatusSeverity.Success, - Message = successMessage + Severity = StatusMessageModel.StatusSeverity.Error, + Message = "Role could not be updated" }); - - return RedirectToAction(nameof(ListRoles), new { storeId }); + return View(viewModel); } - [HttpGet("{storeId}/roles/{role}/delete")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task DeleteRole( - string storeId, - [FromServices] StoreRepository storeRepository, - string role) + TempData.SetStatusMessageModel(new StatusMessageModel() { - var roleData = await storeRepository.GetStoreRole(new StoreRoleId(storeId, role), true); - if (roleData == null) - return NotFound(); + Severity = StatusMessageModel.StatusSeverity.Success, + Message = successMessage + }); - return View("Confirm", - roleData.IsUsed is true - ? new ConfirmModel("Delete role", - $"Unable to proceed: The role {_html.Encode(roleData.Role)} is currently assigned to one or more users, it cannot be removed.") - : new ConfirmModel("Delete role", - $"The role {_html.Encode(roleData.Role)} will be permanently deleted. Are you sure?", - "Delete")); - } + return RedirectToAction(nameof(ListRoles), new { storeId }); + } - [HttpPost("{storeId}/roles/{role}/delete")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task DeleteRolePost( - string storeId, - [FromServices] StoreRepository storeRepository, - string role) + [HttpGet("{storeId}/roles/{role}/delete")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task DeleteRole( + string storeId, + [FromServices] StoreRepository storeRepository, + string role) + { + var roleData = await storeRepository.GetStoreRole(new StoreRoleId(storeId, role), true); + if (roleData == null) + return NotFound(); + + return View("Confirm", + roleData.IsUsed is true + ? new ConfirmModel("Delete role", + $"Unable to proceed: The role {_html.Encode(roleData.Role)} is currently assigned to one or more users, it cannot be removed.") + : new ConfirmModel("Delete role", + $"The role {_html.Encode(roleData.Role)} will be permanently deleted. Are you sure?", + "Delete")); + } + + [HttpPost("{storeId}/roles/{role}/delete")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task DeleteRolePost( + string storeId, + [FromServices] StoreRepository storeRepository, + string role) + { + var roleId = new StoreRoleId(storeId, role); + var roleData = await storeRepository.GetStoreRole(roleId, true); + if (roleData == null) + return NotFound(); + if (roleData.IsUsed is true) { - var roleId = new StoreRoleId(storeId, role); - var roleData = await storeRepository.GetStoreRole(roleId, true); - if (roleData == null) - return NotFound(); - if (roleData.IsUsed is true) - { - return BadRequest(); - } - await storeRepository.RemoveStoreRole(roleId); - - TempData[WellKnownTempData.SuccessMessage] = "Role deleted"; - return RedirectToAction(nameof(ListRoles), new { storeId }); + return BadRequest(); } + await storeRepository.RemoveStoreRole(roleId); + + TempData[WellKnownTempData.SuccessMessage] = "Role deleted"; + return RedirectToAction(nameof(ListRoles), new { storeId }); } } diff --git a/BTCPayServer/Controllers/UIStoresController.Settings.cs b/BTCPayServer/Controllers/UIStoresController.Settings.cs index cb037e163..2e08a1d52 100644 --- a/BTCPayServer/Controllers/UIStoresController.Settings.cs +++ b/BTCPayServer/Controllers/UIStoresController.Settings.cs @@ -14,226 +14,226 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; -namespace BTCPayServer.Controllers +namespace BTCPayServer.Controllers; + +public partial class UIStoresController { - public partial class UIStoresController + [HttpGet("{storeId}/settings")] + public IActionResult GeneralSettings() { - [HttpGet("{storeId}/settings")] - public IActionResult GeneralSettings() + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + + var storeBlob = store.GetStoreBlob(); + var vm = new GeneralSettingsViewModel { - var store = HttpContext.GetStoreData(); - if (store == null) - return NotFound(); + Id = store.Id, + StoreName = store.StoreName, + StoreWebsite = store.StoreWebsite, + LogoFileId = storeBlob.LogoFileId, + CssFileId = storeBlob.CssFileId, + BrandColor = storeBlob.BrandColor, + NetworkFeeMode = storeBlob.NetworkFeeMode, + AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice, + PaymentTolerance = storeBlob.PaymentTolerance, + InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes, + DefaultCurrency = storeBlob.DefaultCurrency, + BOLT11Expiration = (long)storeBlob.RefundBOLT11Expiration.TotalDays, + Archived = store.Archived, + CanDelete = _storeRepo.CanDeleteStores() + }; - var storeBlob = store.GetStoreBlob(); - var vm = new GeneralSettingsViewModel - { - Id = store.Id, - StoreName = store.StoreName, - StoreWebsite = store.StoreWebsite, - LogoFileId = storeBlob.LogoFileId, - CssFileId = storeBlob.CssFileId, - BrandColor = storeBlob.BrandColor, - NetworkFeeMode = storeBlob.NetworkFeeMode, - AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice, - PaymentTolerance = storeBlob.PaymentTolerance, - InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes, - DefaultCurrency = storeBlob.DefaultCurrency, - BOLT11Expiration = (long)storeBlob.RefundBOLT11Expiration.TotalDays, - Archived = store.Archived, - CanDelete = _storeRepo.CanDeleteStores() - }; + return View(vm); + } - return View(vm); + [HttpPost("{storeId}/settings")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task GeneralSettings( + GeneralSettingsViewModel model, + [FromForm] bool RemoveLogoFile = false, + [FromForm] bool RemoveCssFile = false) + { + bool needUpdate = false; + if (CurrentStore.StoreName != model.StoreName) + { + needUpdate = true; + CurrentStore.StoreName = model.StoreName; } - [HttpPost("{storeId}/settings")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task GeneralSettings( - GeneralSettingsViewModel model, - [FromForm] bool RemoveLogoFile = false, - [FromForm] bool RemoveCssFile = false) + if (CurrentStore.StoreWebsite != model.StoreWebsite) { - bool needUpdate = false; - if (CurrentStore.StoreName != model.StoreName) + needUpdate = true; + CurrentStore.StoreWebsite = model.StoreWebsite; + } + + var blob = CurrentStore.GetStoreBlob(); + blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice; + blob.NetworkFeeMode = model.NetworkFeeMode; + blob.PaymentTolerance = model.PaymentTolerance; + blob.DefaultCurrency = model.DefaultCurrency; + blob.InvoiceExpiration = TimeSpan.FromMinutes(model.InvoiceExpiration); + blob.RefundBOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration); + if (!string.IsNullOrEmpty(model.BrandColor) && !ColorPalette.IsValid(model.BrandColor)) + { + ModelState.AddModelError(nameof(model.BrandColor), "Invalid color"); + return View(model); + } + blob.BrandColor = model.BrandColor; + + var userId = GetUserId(); + if (userId is null) + return NotFound(); + + if (model.LogoFile != null) + { + if (model.LogoFile.Length > 1_000_000) { - needUpdate = true; - CurrentStore.StoreName = model.StoreName; + ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file should be less than 1MB"); } - - if (CurrentStore.StoreWebsite != model.StoreWebsite) + else if (!model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture)) { - needUpdate = true; - CurrentStore.StoreWebsite = model.StoreWebsite; + ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file needs to be an image"); } - - var blob = CurrentStore.GetStoreBlob(); - blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice; - blob.NetworkFeeMode = model.NetworkFeeMode; - blob.PaymentTolerance = model.PaymentTolerance; - blob.DefaultCurrency = model.DefaultCurrency; - blob.InvoiceExpiration = TimeSpan.FromMinutes(model.InvoiceExpiration); - blob.RefundBOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration); - if (!string.IsNullOrEmpty(model.BrandColor) && !ColorPalette.IsValid(model.BrandColor)) + else { - ModelState.AddModelError(nameof(model.BrandColor), "Invalid color"); - return View(model); - } - blob.BrandColor = model.BrandColor; - - var userId = GetUserId(); - if (userId is null) - return NotFound(); - - if (model.LogoFile != null) - { - if (model.LogoFile.Length > 1_000_000) - { - ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file should be less than 1MB"); - } - else if (!model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture)) + var formFile = await model.LogoFile.Bufferize(); + if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName)) { ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file needs to be an image"); } else { - var formFile = await model.LogoFile.Bufferize(); - if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName)) - { - ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file needs to be an image"); - } - else - { - model.LogoFile = formFile; - // delete existing file - if (!string.IsNullOrEmpty(blob.LogoFileId)) - { - await _fileService.RemoveFile(blob.LogoFileId, userId); - } - // add new image - try - { - var storedFile = await _fileService.AddFile(model.LogoFile, userId); - blob.LogoFileId = storedFile.Id; - } - catch (Exception e) - { - ModelState.AddModelError(nameof(model.LogoFile), $"Could not save logo: {e.Message}"); - } - } - } - } - else if (RemoveLogoFile && !string.IsNullOrEmpty(blob.LogoFileId)) - { - await _fileService.RemoveFile(blob.LogoFileId, userId); - blob.LogoFileId = null; - needUpdate = true; - } - - if (model.CssFile != null) - { - if (model.CssFile.Length > 1_000_000) - { - ModelState.AddModelError(nameof(model.CssFile), "The uploaded file should be less than 1MB"); - } - else if (!model.CssFile.ContentType.Equals("text/css", StringComparison.InvariantCulture)) - { - ModelState.AddModelError(nameof(model.CssFile), "The uploaded file needs to be a CSS file"); - } - else if (!model.CssFile.FileName.EndsWith(".css", StringComparison.OrdinalIgnoreCase)) - { - ModelState.AddModelError(nameof(model.CssFile), "The uploaded file needs to be a CSS file"); - } - else - { + model.LogoFile = formFile; // delete existing file - if (!string.IsNullOrEmpty(blob.CssFileId)) + if (!string.IsNullOrEmpty(blob.LogoFileId)) { - await _fileService.RemoveFile(blob.CssFileId, userId); + await _fileService.RemoveFile(blob.LogoFileId, userId); } - // add new file + // add new image try { - var storedFile = await _fileService.AddFile(model.CssFile, userId); - blob.CssFileId = storedFile.Id; + var storedFile = await _fileService.AddFile(model.LogoFile, userId); + blob.LogoFileId = storedFile.Id; } catch (Exception e) { - ModelState.AddModelError(nameof(model.CssFile), $"Could not save CSS file: {e.Message}"); + ModelState.AddModelError(nameof(model.LogoFile), $"Could not save logo: {e.Message}"); } } } - else if (RemoveCssFile && !string.IsNullOrEmpty(blob.CssFileId)) - { - await _fileService.RemoveFile(blob.CssFileId, userId); - blob.CssFileId = null; - needUpdate = true; - } - - if (CurrentStore.SetStoreBlob(blob)) - { - needUpdate = true; - } - - if (needUpdate) - { - await _storeRepo.UpdateStore(CurrentStore); - - TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated"; - } - - return RedirectToAction(nameof(GeneralSettings), new - { - storeId = CurrentStore.Id - }); } - - [HttpPost("{storeId}/archive")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task ToggleArchive(string storeId) + else if (RemoveLogoFile && !string.IsNullOrEmpty(blob.LogoFileId)) + { + await _fileService.RemoveFile(blob.LogoFileId, userId); + blob.LogoFileId = null; + needUpdate = true; + } + + if (model.CssFile != null) + { + if (model.CssFile.Length > 1_000_000) + { + ModelState.AddModelError(nameof(model.CssFile), "The uploaded file should be less than 1MB"); + } + else if (!model.CssFile.ContentType.Equals("text/css", StringComparison.InvariantCulture)) + { + ModelState.AddModelError(nameof(model.CssFile), "The uploaded file needs to be a CSS file"); + } + else if (!model.CssFile.FileName.EndsWith(".css", StringComparison.OrdinalIgnoreCase)) + { + ModelState.AddModelError(nameof(model.CssFile), "The uploaded file needs to be a CSS file"); + } + else + { + // delete existing file + if (!string.IsNullOrEmpty(blob.CssFileId)) + { + await _fileService.RemoveFile(blob.CssFileId, userId); + } + // add new file + try + { + var storedFile = await _fileService.AddFile(model.CssFile, userId); + blob.CssFileId = storedFile.Id; + } + catch (Exception e) + { + ModelState.AddModelError(nameof(model.CssFile), $"Could not save CSS file: {e.Message}"); + } + } + } + else if (RemoveCssFile && !string.IsNullOrEmpty(blob.CssFileId)) + { + await _fileService.RemoveFile(blob.CssFileId, userId); + blob.CssFileId = null; + needUpdate = true; + } + + if (CurrentStore.SetStoreBlob(blob)) + { + needUpdate = true; + } + + if (needUpdate) { - CurrentStore.Archived = !CurrentStore.Archived; await _storeRepo.UpdateStore(CurrentStore); - TempData[WellKnownTempData.SuccessMessage] = CurrentStore.Archived - ? "The store has been archived and will no longer appear in the stores list by default." - : "The store has been unarchived and will appear in the stores list by default again."; - - return RedirectToAction(nameof(GeneralSettings), new - { - storeId = CurrentStore.Id - }); + TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated"; } - [HttpGet("{storeId}/delete")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public IActionResult DeleteStore(string storeId) + return RedirectToAction(nameof(GeneralSettings), new { - return View("Confirm", new ConfirmModel("Delete store", "The store will be permanently deleted. This action will also delete all invoices, apps and data associated with the store. Are you sure?", "Delete")); - } - - [HttpPost("{storeId}/delete")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task DeleteStorePost(string storeId) - { - await _storeRepo.DeleteStore(CurrentStore.Id); - TempData[WellKnownTempData.SuccessMessage] = "Store successfully deleted."; - return RedirectToAction(nameof(UIHomeController.Index), "UIHome"); - } + storeId = CurrentStore.Id + }); + } - [HttpGet("{storeId}/checkout")] - public IActionResult CheckoutAppearance() + [HttpPost("{storeId}/archive")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task ToggleArchive(string storeId) + { + CurrentStore.Archived = !CurrentStore.Archived; + await _storeRepo.UpdateStore(CurrentStore); + + TempData[WellKnownTempData.SuccessMessage] = CurrentStore.Archived + ? "The store has been archived and will no longer appear in the stores list by default." + : "The store has been unarchived and will appear in the stores list by default again."; + + return RedirectToAction(nameof(GeneralSettings), new { - var storeBlob = CurrentStore.GetStoreBlob(); - var vm = new CheckoutAppearanceViewModel(); - SetCryptoCurrencies(vm, CurrentStore); - vm.PaymentMethodCriteria = CurrentStore.GetPaymentMethodConfigs(_handlers) - .Where(s => !storeBlob.GetExcludedPaymentMethods().Match(s.Key) && s.Value is not LNURLPaymentMethodConfig) - .Select(c => + storeId = CurrentStore.Id + }); + } + + [HttpGet("{storeId}/delete")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public IActionResult DeleteStore(string storeId) + { + return View("Confirm", new ConfirmModel("Delete store", "The store will be permanently deleted. This action will also delete all invoices, apps and data associated with the store. Are you sure?", "Delete")); + } + + [HttpPost("{storeId}/delete")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task DeleteStorePost(string storeId) + { + await _storeRepo.DeleteStore(CurrentStore.Id); + TempData[WellKnownTempData.SuccessMessage] = "Store successfully deleted."; + return RedirectToAction(nameof(UIHomeController.Index), "UIHome"); + } + + [HttpGet("{storeId}/checkout")] + public IActionResult CheckoutAppearance() + { + var storeBlob = CurrentStore.GetStoreBlob(); + var vm = new CheckoutAppearanceViewModel(); + SetCryptoCurrencies(vm, CurrentStore); + vm.PaymentMethodCriteria = CurrentStore.GetPaymentMethodConfigs(_handlers) + .Where(s => !storeBlob.GetExcludedPaymentMethods().Match(s.Key) && s.Value is not LNURLPaymentMethodConfig) + .Select(c => { var pmi = c.Key; var existing = storeBlob.PaymentMethodCriteria.SingleOrDefault(criteria => - criteria.PaymentMethod == pmi); + criteria.PaymentMethod == pmi); return existing is null ? new PaymentMethodCriteriaViewModel { PaymentMethod = pmi.ToString(), Value = "" } : new PaymentMethodCriteriaViewModel @@ -246,202 +246,201 @@ namespace BTCPayServer.Controllers }; }).ToList(); - vm.UseClassicCheckout = storeBlob.CheckoutType == Client.Models.CheckoutType.V1; - vm.CelebratePayment = storeBlob.CelebratePayment; - vm.PlaySoundOnPayment = storeBlob.PlaySoundOnPayment; - vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback; - vm.ShowPayInWalletButton = storeBlob.ShowPayInWalletButton; - vm.ShowStoreHeader = storeBlob.ShowStoreHeader; - vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi; - vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail; - vm.LazyPaymentMethods = storeBlob.LazyPaymentMethods; - vm.RedirectAutomatically = storeBlob.RedirectAutomatically; - vm.CustomCSS = storeBlob.CustomCSS; - vm.CustomLogo = storeBlob.CustomLogo; - vm.SoundFileId = storeBlob.SoundFileId; - vm.HtmlTitle = storeBlob.HtmlTitle; - vm.SupportUrl = storeBlob.StoreSupportUrl; - vm.DisplayExpirationTimer = (int)storeBlob.DisplayExpirationTimer.TotalMinutes; - vm.ReceiptOptions = CheckoutAppearanceViewModel.ReceiptOptionsViewModel.Create(storeBlob.ReceiptOptions); - vm.AutoDetectLanguage = storeBlob.AutoDetectLanguage; - vm.SetLanguages(_langService, storeBlob.DefaultLang); + vm.UseClassicCheckout = storeBlob.CheckoutType == Client.Models.CheckoutType.V1; + vm.CelebratePayment = storeBlob.CelebratePayment; + vm.PlaySoundOnPayment = storeBlob.PlaySoundOnPayment; + vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback; + vm.ShowPayInWalletButton = storeBlob.ShowPayInWalletButton; + vm.ShowStoreHeader = storeBlob.ShowStoreHeader; + vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi; + vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail; + vm.LazyPaymentMethods = storeBlob.LazyPaymentMethods; + vm.RedirectAutomatically = storeBlob.RedirectAutomatically; + vm.CustomCSS = storeBlob.CustomCSS; + vm.CustomLogo = storeBlob.CustomLogo; + vm.SoundFileId = storeBlob.SoundFileId; + vm.HtmlTitle = storeBlob.HtmlTitle; + vm.SupportUrl = storeBlob.StoreSupportUrl; + vm.DisplayExpirationTimer = (int)storeBlob.DisplayExpirationTimer.TotalMinutes; + vm.ReceiptOptions = CheckoutAppearanceViewModel.ReceiptOptionsViewModel.Create(storeBlob.ReceiptOptions); + vm.AutoDetectLanguage = storeBlob.AutoDetectLanguage; + vm.SetLanguages(_langService, storeBlob.DefaultLang); - return View(vm); - } + return View(vm); + } - [HttpPost("{storeId}/checkout")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task CheckoutAppearance(CheckoutAppearanceViewModel model, [FromForm] bool RemoveSoundFile = false) + [HttpPost("{storeId}/checkout")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task CheckoutAppearance(CheckoutAppearanceViewModel model, [FromForm] bool RemoveSoundFile = false) + { + bool needUpdate = false; + var blob = CurrentStore.GetStoreBlob(); + var defaultPaymentMethodId = model.DefaultPaymentMethod == null ? null : PaymentMethodId.Parse(model.DefaultPaymentMethod); + if (CurrentStore.GetDefaultPaymentId() != defaultPaymentMethodId) { - bool needUpdate = false; - var blob = CurrentStore.GetStoreBlob(); - var defaultPaymentMethodId = model.DefaultPaymentMethod == null ? null : PaymentMethodId.Parse(model.DefaultPaymentMethod); - if (CurrentStore.GetDefaultPaymentId() != defaultPaymentMethodId) + needUpdate = true; + CurrentStore.SetDefaultPaymentId(defaultPaymentMethodId); + } + SetCryptoCurrencies(model, CurrentStore); + model.SetLanguages(_langService, model.DefaultLang); + model.PaymentMethodCriteria ??= new List(); + for (var index = 0; index < model.PaymentMethodCriteria.Count; index++) + { + var methodCriterion = model.PaymentMethodCriteria[index]; + if (!string.IsNullOrWhiteSpace(methodCriterion.Value)) { - needUpdate = true; - CurrentStore.SetDefaultPaymentId(defaultPaymentMethodId); - } - SetCryptoCurrencies(model, CurrentStore); - model.SetLanguages(_langService, model.DefaultLang); - model.PaymentMethodCriteria ??= new List(); - for (var index = 0; index < model.PaymentMethodCriteria.Count; index++) - { - var methodCriterion = model.PaymentMethodCriteria[index]; - if (!string.IsNullOrWhiteSpace(methodCriterion.Value)) + if (!CurrencyValue.TryParse(methodCriterion.Value, out _)) { - if (!CurrencyValue.TryParse(methodCriterion.Value, out _)) - { - model.AddModelError(viewModel => viewModel.PaymentMethodCriteria[index].Value, - $"{methodCriterion.PaymentMethod}: Invalid format. Make sure to enter a valid amount and currency code. Examples: '5 USD', '0.001 BTC'", this); - } + model.AddModelError(viewModel => viewModel.PaymentMethodCriteria[index].Value, + $"{methodCriterion.PaymentMethod}: Invalid format. Make sure to enter a valid amount and currency code. Examples: '5 USD', '0.001 BTC'", this); } } + } - var userId = GetUserId(); - if (userId is null) - return NotFound(); + var userId = GetUserId(); + if (userId is null) + return NotFound(); - if (model.SoundFile != null) + if (model.SoundFile != null) + { + if (model.SoundFile.Length > 1_000_000) { - if (model.SoundFile.Length > 1_000_000) - { - ModelState.AddModelError(nameof(model.SoundFile), "The uploaded sound file should be less than 1MB"); - } - else if (!model.SoundFile.ContentType.StartsWith("audio/", StringComparison.InvariantCulture)) + ModelState.AddModelError(nameof(model.SoundFile), "The uploaded sound file should be less than 1MB"); + } + else if (!model.SoundFile.ContentType.StartsWith("audio/", StringComparison.InvariantCulture)) + { + ModelState.AddModelError(nameof(model.SoundFile), "The uploaded sound file needs to be an audio file"); + } + else + { + var formFile = await model.SoundFile.Bufferize(); + if (!FileTypeDetector.IsAudio(formFile.Buffer, formFile.FileName)) { ModelState.AddModelError(nameof(model.SoundFile), "The uploaded sound file needs to be an audio file"); } else { - var formFile = await model.SoundFile.Bufferize(); - if (!FileTypeDetector.IsAudio(formFile.Buffer, formFile.FileName)) + model.SoundFile = formFile; + // delete existing file + if (!string.IsNullOrEmpty(blob.SoundFileId)) { - ModelState.AddModelError(nameof(model.SoundFile), "The uploaded sound file needs to be an audio file"); + await _fileService.RemoveFile(blob.SoundFileId, userId); } - else - { - model.SoundFile = formFile; - // delete existing file - if (!string.IsNullOrEmpty(blob.SoundFileId)) - { - await _fileService.RemoveFile(blob.SoundFileId, userId); - } - // add new file - try - { - var storedFile = await _fileService.AddFile(model.SoundFile, userId); - blob.SoundFileId = storedFile.Id; - needUpdate = true; - } - catch (Exception e) - { - ModelState.AddModelError(nameof(model.SoundFile), $"Could not save sound: {e.Message}"); - } + // add new file + try + { + var storedFile = await _fileService.AddFile(model.SoundFile, userId); + blob.SoundFileId = storedFile.Id; + needUpdate = true; + } + catch (Exception e) + { + ModelState.AddModelError(nameof(model.SoundFile), $"Could not save sound: {e.Message}"); } } } - else if (RemoveSoundFile && !string.IsNullOrEmpty(blob.SoundFileId)) - { - await _fileService.RemoveFile(blob.SoundFileId, userId); - blob.SoundFileId = null; - needUpdate = true; - } + } + else if (RemoveSoundFile && !string.IsNullOrEmpty(blob.SoundFileId)) + { + await _fileService.RemoveFile(blob.SoundFileId, userId); + blob.SoundFileId = null; + needUpdate = true; + } - if (!ModelState.IsValid) - { - return View(model); - } + if (!ModelState.IsValid) + { + return View(model); + } - // Payment criteria for Off-Chain should also affect LNUrl - foreach (var newCriteria in model.PaymentMethodCriteria.ToList()) - { - var paymentMethodId = PaymentMethodId.Parse(newCriteria.PaymentMethod); - if (_handlers.TryGet(paymentMethodId) is LightningLikePaymentHandler h) - model.PaymentMethodCriteria.Add(new PaymentMethodCriteriaViewModel - { - PaymentMethod = PaymentTypes.LNURL.GetPaymentMethodId(h.Network.CryptoCode).ToString(), - Type = newCriteria.Type, - Value = newCriteria.Value - }); - // Should not be able to set LNUrlPay criteria directly in UI - if (_handlers.TryGet(paymentMethodId) is LNURLPayPaymentHandler) - model.PaymentMethodCriteria.Remove(newCriteria); - } - blob.PaymentMethodCriteria ??= new List(); - foreach (var newCriteria in model.PaymentMethodCriteria) - { - var paymentMethodId = PaymentMethodId.Parse(newCriteria.PaymentMethod); - var existingCriteria = blob.PaymentMethodCriteria.FirstOrDefault(c => c.PaymentMethod == paymentMethodId); - if (existingCriteria != null) - blob.PaymentMethodCriteria.Remove(existingCriteria); - CurrencyValue.TryParse(newCriteria.Value, out var cv); - blob.PaymentMethodCriteria.Add(new PaymentMethodCriteria() + // Payment criteria for Off-Chain should also affect LNUrl + foreach (var newCriteria in model.PaymentMethodCriteria.ToList()) + { + var paymentMethodId = PaymentMethodId.Parse(newCriteria.PaymentMethod); + if (_handlers.TryGet(paymentMethodId) is LightningLikePaymentHandler h) + model.PaymentMethodCriteria.Add(new PaymentMethodCriteriaViewModel { - Above = newCriteria.Type == PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan, - Value = cv, - PaymentMethod = paymentMethodId + PaymentMethod = PaymentTypes.LNURL.GetPaymentMethodId(h.Network.CryptoCode).ToString(), + Type = newCriteria.Type, + Value = newCriteria.Value }); - } - - blob.ShowPayInWalletButton = model.ShowPayInWalletButton; - blob.ShowStoreHeader = model.ShowStoreHeader; - blob.CheckoutType = model.UseClassicCheckout ? Client.Models.CheckoutType.V1 : Client.Models.CheckoutType.V2; - blob.CelebratePayment = model.CelebratePayment; - blob.PlaySoundOnPayment = model.PlaySoundOnPayment; - blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback; - blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi; - blob.RequiresRefundEmail = model.RequiresRefundEmail; - blob.LazyPaymentMethods = model.LazyPaymentMethods; - blob.RedirectAutomatically = model.RedirectAutomatically; - blob.ReceiptOptions = model.ReceiptOptions.ToDTO(); - blob.CustomLogo = model.CustomLogo; - blob.CustomCSS = model.CustomCSS; - blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle; - blob.StoreSupportUrl = string.IsNullOrWhiteSpace(model.SupportUrl) ? null : model.SupportUrl.IsValidEmail() ? $"mailto:{model.SupportUrl}" : model.SupportUrl; - blob.DisplayExpirationTimer = TimeSpan.FromMinutes(model.DisplayExpirationTimer); - blob.AutoDetectLanguage = model.AutoDetectLanguage; - blob.DefaultLang = model.DefaultLang; - blob.NormalizeToRelativeLinks(Request); - if (CurrentStore.SetStoreBlob(blob)) + // Should not be able to set LNUrlPay criteria directly in UI + if (_handlers.TryGet(paymentMethodId) is LNURLPayPaymentHandler) + model.PaymentMethodCriteria.Remove(newCriteria); + } + blob.PaymentMethodCriteria ??= new List(); + foreach (var newCriteria in model.PaymentMethodCriteria) + { + var paymentMethodId = PaymentMethodId.Parse(newCriteria.PaymentMethod); + var existingCriteria = blob.PaymentMethodCriteria.FirstOrDefault(c => c.PaymentMethod == paymentMethodId); + if (existingCriteria != null) + blob.PaymentMethodCriteria.Remove(existingCriteria); + CurrencyValue.TryParse(newCriteria.Value, out var cv); + blob.PaymentMethodCriteria.Add(new PaymentMethodCriteria() { - needUpdate = true; - } - if (needUpdate) - { - await _storeRepo.UpdateStore(CurrentStore); - TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated"; - } - - return RedirectToAction(nameof(CheckoutAppearance), new - { - storeId = CurrentStore.Id + Above = newCriteria.Type == PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan, + Value = cv, + PaymentMethod = paymentMethodId }); } - void SetCryptoCurrencies(CheckoutAppearanceViewModel vm, StoreData storeData) + blob.ShowPayInWalletButton = model.ShowPayInWalletButton; + blob.ShowStoreHeader = model.ShowStoreHeader; + blob.CheckoutType = model.UseClassicCheckout ? Client.Models.CheckoutType.V1 : Client.Models.CheckoutType.V2; + blob.CelebratePayment = model.CelebratePayment; + blob.PlaySoundOnPayment = model.PlaySoundOnPayment; + blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback; + blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi; + blob.RequiresRefundEmail = model.RequiresRefundEmail; + blob.LazyPaymentMethods = model.LazyPaymentMethods; + blob.RedirectAutomatically = model.RedirectAutomatically; + blob.ReceiptOptions = model.ReceiptOptions.ToDTO(); + blob.CustomLogo = model.CustomLogo; + blob.CustomCSS = model.CustomCSS; + blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle; + blob.StoreSupportUrl = string.IsNullOrWhiteSpace(model.SupportUrl) ? null : model.SupportUrl.IsValidEmail() ? $"mailto:{model.SupportUrl}" : model.SupportUrl; + blob.DisplayExpirationTimer = TimeSpan.FromMinutes(model.DisplayExpirationTimer); + blob.AutoDetectLanguage = model.AutoDetectLanguage; + blob.DefaultLang = model.DefaultLang; + blob.NormalizeToRelativeLinks(Request); + if (CurrentStore.SetStoreBlob(blob)) { - var choices = GetEnabledPaymentMethodChoices(storeData); - var chosen = GetDefaultPaymentMethodChoice(storeData); - - vm.PaymentMethods = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen?.Value); - vm.DefaultPaymentMethod = chosen?.Value; + needUpdate = true; + } + if (needUpdate) + { + await _storeRepo.UpdateStore(CurrentStore); + TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated"; } - PaymentMethodOptionViewModel.Format? GetDefaultPaymentMethodChoice(StoreData storeData) + return RedirectToAction(nameof(CheckoutAppearance), new { - var enabled = storeData.GetEnabledPaymentIds(); - var defaultPaymentId = storeData.GetDefaultPaymentId(); - var defaultChoice = defaultPaymentId?.FindNearest(enabled); - if (defaultChoice is null) - { - defaultChoice = enabled.FirstOrDefault(e => e == PaymentTypes.CHAIN.GetPaymentMethodId(_networkProvider.DefaultNetwork.CryptoCode)) ?? - enabled.FirstOrDefault(e => e == PaymentTypes.LN.GetPaymentMethodId(_networkProvider.DefaultNetwork.CryptoCode)) ?? - enabled.FirstOrDefault(); - } - var choices = GetEnabledPaymentMethodChoices(storeData); + storeId = CurrentStore.Id + }); + } - return defaultChoice is null ? null : choices.FirstOrDefault(c => defaultChoice.ToString().Equals(c.Value, StringComparison.OrdinalIgnoreCase)); + void SetCryptoCurrencies(CheckoutAppearanceViewModel vm, StoreData storeData) + { + var choices = GetEnabledPaymentMethodChoices(storeData); + var chosen = GetDefaultPaymentMethodChoice(storeData); + + vm.PaymentMethods = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen?.Value); + vm.DefaultPaymentMethod = chosen?.Value; + } + + PaymentMethodOptionViewModel.Format? GetDefaultPaymentMethodChoice(StoreData storeData) + { + var enabled = storeData.GetEnabledPaymentIds(); + var defaultPaymentId = storeData.GetDefaultPaymentId(); + var defaultChoice = defaultPaymentId?.FindNearest(enabled); + if (defaultChoice is null) + { + defaultChoice = enabled.FirstOrDefault(e => e == PaymentTypes.CHAIN.GetPaymentMethodId(_networkProvider.DefaultNetwork.CryptoCode)) ?? + enabled.FirstOrDefault(e => e == PaymentTypes.LN.GetPaymentMethodId(_networkProvider.DefaultNetwork.CryptoCode)) ?? + enabled.FirstOrDefault(); } + var choices = GetEnabledPaymentMethodChoices(storeData); + + return defaultChoice is null ? null : choices.FirstOrDefault(c => defaultChoice.ToString().Equals(c.Value, StringComparison.OrdinalIgnoreCase)); } } diff --git a/BTCPayServer/Controllers/UIStoresController.Tokens.cs b/BTCPayServer/Controllers/UIStoresController.Tokens.cs index c943683cd..b0482b13a 100644 --- a/BTCPayServer/Controllers/UIStoresController.Tokens.cs +++ b/BTCPayServer/Controllers/UIStoresController.Tokens.cs @@ -14,249 +14,248 @@ using Microsoft.AspNetCore.Mvc.Rendering; using NBitcoin; using NBitcoin.DataEncoders; -namespace BTCPayServer.Controllers +namespace BTCPayServer.Controllers; + +public partial class UIStoresController { - public partial class UIStoresController + [HttpGet("{storeId}/tokens")] + [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task ListTokens() { - [HttpGet("{storeId}/tokens")] - [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task ListTokens() + var model = new TokensViewModel(); + var tokens = await _tokenRepository.GetTokensByStoreIdAsync(CurrentStore.Id); + model.StoreNotConfigured = StoreNotConfigured; + model.Tokens = tokens.Select(t => new TokenViewModel() { - var model = new TokensViewModel(); - var tokens = await _tokenRepository.GetTokensByStoreIdAsync(CurrentStore.Id); - model.StoreNotConfigured = StoreNotConfigured; - model.Tokens = tokens.Select(t => new TokenViewModel() - { - Label = t.Label, - SIN = t.SIN, - Id = t.Value - }).ToArray(); - - model.ApiKey = (await _tokenRepository.GetLegacyAPIKeys(CurrentStore.Id)).FirstOrDefault(); - model.EncodedApiKey = model.ApiKey == null ? "*API Key*" : Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(model.ApiKey)); - return View(model); - } - - [HttpGet("{storeId}/tokens/{tokenId}/revoke")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task RevokeToken(string tokenId) - { - var token = await _tokenRepository.GetToken(tokenId); - if (token == null || token.StoreId != CurrentStore.Id) - return NotFound(); - return View("Confirm", new ConfirmModel("Revoke the token", $"The access token with the label {_html.Encode(token.Label)} will be revoked. Do you wish to continue?", "Revoke")); - } - - [HttpPost("{storeId}/tokens/{tokenId}/revoke")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task RevokeTokenConfirm(string tokenId) - { - var token = await _tokenRepository.GetToken(tokenId); - if (token == null || - token.StoreId != CurrentStore.Id || - !await _tokenRepository.DeleteToken(tokenId)) - TempData[WellKnownTempData.ErrorMessage] = "Failure to revoke this token."; - else - TempData[WellKnownTempData.SuccessMessage] = "Token revoked"; - return RedirectToAction(nameof(ListTokens), new { storeId = token?.StoreId }); - } - - [HttpGet("{storeId}/tokens/{tokenId}")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task ShowToken(string tokenId) - { - var token = await _tokenRepository.GetToken(tokenId); - if (token == null || token.StoreId != CurrentStore.Id) - return NotFound(); - return View(token); - } - - [HttpGet("{storeId}/tokens/create")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public IActionResult CreateToken(string storeId) - { - var model = new CreateTokenViewModel(); - ViewBag.HidePublicKey = storeId == null; - ViewBag.ShowStores = storeId == null; - ViewBag.ShowMenu = storeId != null; - model.StoreId = storeId; - return View(model); - } - - [HttpPost("{storeId}/tokens/create")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task CreateToken(string storeId, CreateTokenViewModel model) - { - if (!ModelState.IsValid) - { - return View(nameof(CreateToken), model); - } - model.Label ??= string.Empty; - var userId = GetUserId(); - if (userId == null) - return Challenge(AuthenticationSchemes.Cookie); - var store = model.StoreId switch - { - null => CurrentStore, - _ => await _storeRepo.FindStore(storeId, userId) - }; - if (store == null) - return Challenge(AuthenticationSchemes.Cookie); - var tokenRequest = new TokenRequest() + Label = t.Label, + SIN = t.SIN, + Id = t.Value + }).ToArray(); + + model.ApiKey = (await _tokenRepository.GetLegacyAPIKeys(CurrentStore.Id)).FirstOrDefault(); + model.EncodedApiKey = model.ApiKey == null ? "*API Key*" : Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(model.ApiKey)); + return View(model); + } + + [HttpGet("{storeId}/tokens/{tokenId}/revoke")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task RevokeToken(string tokenId) + { + var token = await _tokenRepository.GetToken(tokenId); + if (token == null || token.StoreId != CurrentStore.Id) + return NotFound(); + return View("Confirm", new ConfirmModel("Revoke the token", $"The access token with the label {_html.Encode(token.Label)} will be revoked. Do you wish to continue?", "Revoke")); + } + + [HttpPost("{storeId}/tokens/{tokenId}/revoke")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task RevokeTokenConfirm(string tokenId) + { + var token = await _tokenRepository.GetToken(tokenId); + if (token == null || + token.StoreId != CurrentStore.Id || + !await _tokenRepository.DeleteToken(tokenId)) + TempData[WellKnownTempData.ErrorMessage] = "Failure to revoke this token."; + else + TempData[WellKnownTempData.SuccessMessage] = "Token revoked"; + return RedirectToAction(nameof(ListTokens), new { storeId = token?.StoreId }); + } + + [HttpGet("{storeId}/tokens/{tokenId}")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task ShowToken(string tokenId) + { + var token = await _tokenRepository.GetToken(tokenId); + if (token == null || token.StoreId != CurrentStore.Id) + return NotFound(); + return View(token); + } + + [HttpGet("{storeId}/tokens/create")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public IActionResult CreateToken(string storeId) + { + var model = new CreateTokenViewModel(); + ViewBag.HidePublicKey = storeId == null; + ViewBag.ShowStores = storeId == null; + ViewBag.ShowMenu = storeId != null; + model.StoreId = storeId; + return View(model); + } + + [HttpPost("{storeId}/tokens/create")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task CreateToken(string storeId, CreateTokenViewModel model) + { + if (!ModelState.IsValid) + { + return View(nameof(CreateToken), model); + } + model.Label ??= string.Empty; + var userId = GetUserId(); + if (userId == null) + return Challenge(AuthenticationSchemes.Cookie); + var store = model.StoreId switch + { + null => CurrentStore, + _ => await _storeRepo.FindStore(storeId, userId) + }; + if (store == null) + return Challenge(AuthenticationSchemes.Cookie); + var tokenRequest = new TokenRequest() + { + Label = model.Label, + Id = model.PublicKey == null ? null : NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(new PubKey(model.PublicKey).Compress()) + }; + + string? pairingCode; + if (model.PublicKey == null) + { + tokenRequest.PairingCode = await _tokenRepository.CreatePairingCodeAsync(); + await _tokenRepository.UpdatePairingCode(new PairingCodeEntity() { + Id = tokenRequest.PairingCode, Label = model.Label, - Id = model.PublicKey == null ? null : NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(new PubKey(model.PublicKey).Compress()) - }; - - string? pairingCode; - if (model.PublicKey == null) - { - tokenRequest.PairingCode = await _tokenRepository.CreatePairingCodeAsync(); - await _tokenRepository.UpdatePairingCode(new PairingCodeEntity() - { - Id = tokenRequest.PairingCode, - Label = model.Label, - }); - await _tokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, store.Id); - pairingCode = tokenRequest.PairingCode; - } - else - { - pairingCode = (await _tokenController.Tokens(tokenRequest)).Data[0].PairingCode; - } - - GeneratedPairingCode = pairingCode; - return RedirectToAction(nameof(RequestPairing), new - { - pairingCode, - selectedStore = storeId }); + await _tokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, store.Id); + pairingCode = tokenRequest.PairingCode; + } + else + { + pairingCode = (await _tokenController.Tokens(tokenRequest)).Data[0].PairingCode; } - [HttpGet("/api-tokens")] - [AllowAnonymous] - public async Task CreateToken() + GeneratedPairingCode = pairingCode; + return RedirectToAction(nameof(RequestPairing), new { - var userId = GetUserId(); - if (string.IsNullOrWhiteSpace(userId)) - return Challenge(AuthenticationSchemes.Cookie); - var model = new CreateTokenViewModel(); - ViewBag.HidePublicKey = true; - ViewBag.ShowStores = true; - ViewBag.ShowMenu = false; - var stores = (await _storeRepo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray(); + pairingCode, + selectedStore = storeId + }); + } - model.Stores = new SelectList(stores, nameof(CurrentStore.Id), nameof(CurrentStore.StoreName)); - if (!model.Stores.Any()) - { - TempData[WellKnownTempData.ErrorMessage] = "You need to be owner of at least one store before pairing"; - return RedirectToAction(nameof(UIHomeController.Index), "UIHome"); - } - return View(model); + [HttpGet("/api-tokens")] + [AllowAnonymous] + public async Task CreateToken() + { + var userId = GetUserId(); + if (string.IsNullOrWhiteSpace(userId)) + return Challenge(AuthenticationSchemes.Cookie); + var model = new CreateTokenViewModel(); + ViewBag.HidePublicKey = true; + ViewBag.ShowStores = true; + ViewBag.ShowMenu = false; + var stores = (await _storeRepo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray(); + + model.Stores = new SelectList(stores, nameof(CurrentStore.Id), nameof(CurrentStore.StoreName)); + if (!model.Stores.Any()) + { + TempData[WellKnownTempData.ErrorMessage] = "You need to be owner of at least one store before pairing"; + return RedirectToAction(nameof(UIHomeController.Index), "UIHome"); + } + return View(model); + } + + [HttpPost("/api-tokens")] + [AllowAnonymous] + public Task CreateToken2(CreateTokenViewModel model) + { + return CreateToken(model.StoreId, model); + } + + [HttpPost("{storeId}/tokens/apikey")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task GenerateAPIKey(string storeId, string command = "") + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + if (command == "revoke") + { + await _tokenRepository.RevokeLegacyAPIKeys(CurrentStore.Id); + TempData[WellKnownTempData.SuccessMessage] = "API Key revoked"; + } + else + { + await _tokenRepository.GenerateLegacyAPIKey(CurrentStore.Id); + TempData[WellKnownTempData.SuccessMessage] = "API Key re-generated"; } - [HttpPost("/api-tokens")] - [AllowAnonymous] - public Task CreateToken2(CreateTokenViewModel model) + return RedirectToAction(nameof(ListTokens), new { - return CreateToken(model.StoreId, model); - } + storeId + }); + } - [HttpPost("{storeId}/tokens/apikey")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task GenerateAPIKey(string storeId, string command = "") + [HttpGet("/api-access-request")] + [AllowAnonymous] + public async Task RequestPairing(string pairingCode, string? selectedStore = null) + { + var userId = GetUserId(); + if (userId == null) + return Challenge(AuthenticationSchemes.Cookie); + + if (pairingCode == null) + return NotFound(); + + if (selectedStore != null) { - var store = HttpContext.GetStoreData(); + var store = await _storeRepo.FindStore(selectedStore, userId); if (store == null) return NotFound(); - if (command == "revoke") - { - await _tokenRepository.RevokeLegacyAPIKeys(CurrentStore.Id); - TempData[WellKnownTempData.SuccessMessage] = "API Key revoked"; - } - else - { - await _tokenRepository.GenerateLegacyAPIKey(CurrentStore.Id); - TempData[WellKnownTempData.SuccessMessage] = "API Key re-generated"; - } + HttpContext.SetStoreData(store); + } + var pairing = await _tokenRepository.GetPairingAsync(pairingCode); + if (pairing == null) + { + TempData[WellKnownTempData.ErrorMessage] = "Unknown pairing code"; + return RedirectToAction(nameof(UIHomeController.Index), "UIHome"); + } + + var stores = (await _storeRepo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray(); + return View(new PairingModel + { + Id = pairing.Id, + Label = pairing.Label, + SIN = pairing.SIN ?? "Server-Initiated Pairing", + StoreId = selectedStore ?? stores.FirstOrDefault()?.Id, + Stores = stores.Select(s => new PairingModel.StoreViewModel + { + Id = s.Id, + Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName + }).ToArray() + }); + } + + [HttpPost("/api-access-request")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task Pair(string pairingCode, string storeId) + { + if (pairingCode == null) + return NotFound(); + var store = CurrentStore; + var pairing = await _tokenRepository.GetPairingAsync(pairingCode); + if (store == null || pairing == null) + return NotFound(); + + var pairingResult = await _tokenRepository.PairWithStoreAsync(pairingCode, store.Id); + if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial) + { + var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods(); + StoreNotConfigured = store.GetPaymentMethodConfigs(_handlers).All(p => excludeFilter.Match(p.Key)); + TempData[WellKnownTempData.SuccessMessage] = "Pairing is successful"; + if (pairingResult == PairingResult.Partial) + TempData[WellKnownTempData.SuccessMessage] = "Server initiated pairing code: " + pairingCode; return RedirectToAction(nameof(ListTokens), new { - storeId + storeId = store.Id, pairingCode }); } - [HttpGet("/api-access-request")] - [AllowAnonymous] - public async Task RequestPairing(string pairingCode, string? selectedStore = null) + TempData[WellKnownTempData.ErrorMessage] = $"Pairing failed ({pairingResult})"; + return RedirectToAction(nameof(ListTokens), new { - var userId = GetUserId(); - if (userId == null) - return Challenge(AuthenticationSchemes.Cookie); - - if (pairingCode == null) - return NotFound(); - - if (selectedStore != null) - { - var store = await _storeRepo.FindStore(selectedStore, userId); - if (store == null) - return NotFound(); - HttpContext.SetStoreData(store); - } - - var pairing = await _tokenRepository.GetPairingAsync(pairingCode); - if (pairing == null) - { - TempData[WellKnownTempData.ErrorMessage] = "Unknown pairing code"; - return RedirectToAction(nameof(UIHomeController.Index), "UIHome"); - } - - var stores = (await _storeRepo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray(); - return View(new PairingModel - { - Id = pairing.Id, - Label = pairing.Label, - SIN = pairing.SIN ?? "Server-Initiated Pairing", - StoreId = selectedStore ?? stores.FirstOrDefault()?.Id, - Stores = stores.Select(s => new PairingModel.StoreViewModel - { - Id = s.Id, - Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName - }).ToArray() - }); - } - - [HttpPost("/api-access-request")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task Pair(string pairingCode, string storeId) - { - if (pairingCode == null) - return NotFound(); - var store = CurrentStore; - var pairing = await _tokenRepository.GetPairingAsync(pairingCode); - if (store == null || pairing == null) - return NotFound(); - - var pairingResult = await _tokenRepository.PairWithStoreAsync(pairingCode, store.Id); - if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial) - { - var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods(); - StoreNotConfigured = store.GetPaymentMethodConfigs(_handlers).All(p => excludeFilter.Match(p.Key)); - TempData[WellKnownTempData.SuccessMessage] = "Pairing is successful"; - if (pairingResult == PairingResult.Partial) - TempData[WellKnownTempData.SuccessMessage] = "Server initiated pairing code: " + pairingCode; - return RedirectToAction(nameof(ListTokens), new - { - storeId = store.Id, pairingCode - }); - } - - TempData[WellKnownTempData.ErrorMessage] = $"Pairing failed ({pairingResult})"; - return RedirectToAction(nameof(ListTokens), new - { - storeId = store.Id - }); - } + storeId = store.Id + }); } } diff --git a/BTCPayServer/Controllers/UIStoresController.Users.cs b/BTCPayServer/Controllers/UIStoresController.Users.cs index dcf7a87e0..320809fa6 100644 --- a/BTCPayServer/Controllers/UIStoresController.Users.cs +++ b/BTCPayServer/Controllers/UIStoresController.Users.cs @@ -14,135 +14,134 @@ using BTCPayServer.Services.Stores; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace BTCPayServer.Controllers +namespace BTCPayServer.Controllers; + +public partial class UIStoresController { - public partial class UIStoresController + [HttpGet("{storeId}/users")] + public async Task StoreUsers() { - [HttpGet("{storeId}/users")] - public async Task StoreUsers() + var vm = new StoreUsersViewModel { Role = StoreRoleId.Employee.Role }; + await FillUsers(vm); + return View(vm); + } + + [HttpPost("{storeId}/users")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task StoreUsers(string storeId, StoreUsersViewModel vm) + { + await FillUsers(vm); + if (!ModelState.IsValid) { - var vm = new StoreUsersViewModel { Role = StoreRoleId.Employee.Role }; - await FillUsers(vm); return View(vm); } - [HttpPost("{storeId}/users")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task StoreUsers(string storeId, StoreUsersViewModel vm) + var roles = await _storeRepo.GetStoreRoles(CurrentStore.Id); + if (roles.All(role => role.Id != vm.Role)) { - await FillUsers(vm); - if (!ModelState.IsValid) - { - return View(vm); - } - - var roles = await _storeRepo.GetStoreRoles(CurrentStore.Id); - if (roles.All(role => role.Id != vm.Role)) - { - ModelState.AddModelError(nameof(vm.Role), "Invalid role"); - return View(vm); - } + ModelState.AddModelError(nameof(vm.Role), "Invalid role"); + return View(vm); + } - var user = await _userManager.FindByEmailAsync(vm.Email); - var isExistingUser = user is not null; - var isExistingStoreUser = isExistingUser && await _storeRepo.GetStoreUser(storeId, user!.Id) is not null; - var successInfo = string.Empty; - if (user == null) + var user = await _userManager.FindByEmailAsync(vm.Email); + var isExistingUser = user is not null; + var isExistingStoreUser = isExistingUser && await _storeRepo.GetStoreUser(storeId, user!.Id) is not null; + var successInfo = string.Empty; + if (user == null) + { + user = new ApplicationUser { - user = new ApplicationUser - { - UserName = vm.Email, - Email = vm.Email, - RequiresEmailConfirmation = _policiesSettings.RequiresConfirmedEmail, - RequiresApproval = _policiesSettings.RequiresUserApproval, - Created = DateTimeOffset.UtcNow - }; + UserName = vm.Email, + Email = vm.Email, + RequiresEmailConfirmation = _policiesSettings.RequiresConfirmedEmail, + RequiresApproval = _policiesSettings.RequiresUserApproval, + Created = DateTimeOffset.UtcNow + }; - var result = await _userManager.CreateAsync(user); - if (result.Succeeded) - { - var tcs = new TaskCompletionSource(); - var currentUser = await _userManager.GetUserAsync(HttpContext.User); - - _eventAggregator.Publish(new UserRegisteredEvent - { - RequestUri = Request.GetAbsoluteRootUri(), - Kind = UserRegisteredEventKind.Invite, - User = user, - InvitedByUser = currentUser, - CallbackUrlGenerated = tcs - }); - - var callbackUrl = await tcs.Task; - var settings = await _settingsRepository.GetSettingAsync() ?? new EmailSettings(); - var info = settings.IsComplete() - ? "An invitation email has been sent.
You may alternatively" - : "An invitation email has not been sent, because the server does not have an email server configured.
You need to"; - successInfo = $"{info} share this link with them: {callbackUrl}"; - } - else - { - ModelState.AddModelError(nameof(vm.Email), "User could not be invited"); - return View(vm); - } - } - - var roleId = await _storeRepo.ResolveStoreRoleId(storeId, vm.Role); - var action = isExistingUser - ? isExistingStoreUser ? "updated" : "added" - : "invited"; - if (await _storeRepo.AddOrUpdateStoreUser(CurrentStore.Id, user.Id, roleId)) + var result = await _userManager.CreateAsync(user); + if (result.Succeeded) { - TempData.SetStatusMessageModel(new StatusMessageModel + var tcs = new TaskCompletionSource(); + var currentUser = await _userManager.GetUserAsync(HttpContext.User); + + _eventAggregator.Publish(new UserRegisteredEvent { - Severity = StatusMessageModel.StatusSeverity.Success, - AllowDismiss = false, - Html = $"User {action} successfully." + (string.IsNullOrEmpty(successInfo) ? "" : $" {successInfo}") + RequestUri = Request.GetAbsoluteRootUri(), + Kind = UserRegisteredEventKind.Invite, + User = user, + InvitedByUser = currentUser, + CallbackUrlGenerated = tcs }); - return RedirectToAction(nameof(StoreUsers)); + + var callbackUrl = await tcs.Task; + var settings = await _settingsRepository.GetSettingAsync() ?? new EmailSettings(); + var info = settings.IsComplete() + ? "An invitation email has been sent.
You may alternatively" + : "An invitation email has not been sent, because the server does not have an email server configured.
You need to"; + successInfo = $"{info} share this link with them: {callbackUrl}"; } - - ModelState.AddModelError(nameof(vm.Email), $"The user could not be {action}"); - return View(vm); - } - - [HttpPost("{storeId}/users/{userId}")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task UpdateStoreUser(string storeId, string userId, StoreUsersViewModel.StoreUserViewModel vm) - { - var roleId = await _storeRepo.ResolveStoreRoleId(storeId, vm.Role); - var storeUsers = await _storeRepo.GetStoreUsers(storeId); - var user = storeUsers.First(user => user.Id == userId); - var isOwner = user.StoreRole.Id == StoreRoleId.Owner.Id; - var isLastOwner = isOwner && storeUsers.Count(u => u.StoreRole.Id == StoreRoleId.Owner.Id) == 1; - if (isLastOwner && roleId != StoreRoleId.Owner) - TempData[WellKnownTempData.ErrorMessage] = $"User {user.Email} is the last owner. Their role cannot be changed."; - else if (await _storeRepo.AddOrUpdateStoreUser(storeId, userId, roleId)) - TempData[WellKnownTempData.SuccessMessage] = $"The role of {user.Email} has been changed to {vm.Role}."; - return RedirectToAction(nameof(StoreUsers), new { storeId, userId }); - } - - [HttpPost("{storeId}/users/{userId}/delete")] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public async Task DeleteStoreUser(string storeId, string userId) - { - if (await _storeRepo.RemoveStoreUser(storeId, userId)) - TempData[WellKnownTempData.SuccessMessage] = "User removed successfully."; else - TempData[WellKnownTempData.ErrorMessage] = "Removing this user would result in the store having no owner."; - return RedirectToAction(nameof(StoreUsers), new { storeId, userId }); + { + ModelState.AddModelError(nameof(vm.Email), "User could not be invited"); + return View(vm); + } } - private async Task FillUsers(StoreUsersViewModel vm) + var roleId = await _storeRepo.ResolveStoreRoleId(storeId, vm.Role); + var action = isExistingUser + ? isExistingStoreUser ? "updated" : "added" + : "invited"; + if (await _storeRepo.AddOrUpdateStoreUser(CurrentStore.Id, user.Id, roleId)) { - var users = await _storeRepo.GetStoreUsers(CurrentStore.Id); - vm.StoreId = CurrentStore.Id; - vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel() + TempData.SetStatusMessageModel(new StatusMessageModel { - Email = u.Email, - Id = u.Id, - Role = u.StoreRole.Role - }).ToList(); + Severity = StatusMessageModel.StatusSeverity.Success, + AllowDismiss = false, + Html = $"User {action} successfully." + (string.IsNullOrEmpty(successInfo) ? "" : $" {successInfo}") + }); + return RedirectToAction(nameof(StoreUsers)); } + + ModelState.AddModelError(nameof(vm.Email), $"The user could not be {action}"); + return View(vm); + } + + [HttpPost("{storeId}/users/{userId}")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task UpdateStoreUser(string storeId, string userId, StoreUsersViewModel.StoreUserViewModel vm) + { + var roleId = await _storeRepo.ResolveStoreRoleId(storeId, vm.Role); + var storeUsers = await _storeRepo.GetStoreUsers(storeId); + var user = storeUsers.First(user => user.Id == userId); + var isOwner = user.StoreRole.Id == StoreRoleId.Owner.Id; + var isLastOwner = isOwner && storeUsers.Count(u => u.StoreRole.Id == StoreRoleId.Owner.Id) == 1; + if (isLastOwner && roleId != StoreRoleId.Owner) + TempData[WellKnownTempData.ErrorMessage] = $"User {user.Email} is the last owner. Their role cannot be changed."; + else if (await _storeRepo.AddOrUpdateStoreUser(storeId, userId, roleId)) + TempData[WellKnownTempData.SuccessMessage] = $"The role of {user.Email} has been changed to {vm.Role}."; + return RedirectToAction(nameof(StoreUsers), new { storeId, userId }); + } + + [HttpPost("{storeId}/users/{userId}/delete")] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public async Task DeleteStoreUser(string storeId, string userId) + { + if (await _storeRepo.RemoveStoreUser(storeId, userId)) + TempData[WellKnownTempData.SuccessMessage] = "User removed successfully."; + else + TempData[WellKnownTempData.ErrorMessage] = "Removing this user would result in the store having no owner."; + return RedirectToAction(nameof(StoreUsers), new { storeId, userId }); + } + + private async Task FillUsers(StoreUsersViewModel vm) + { + var users = await _storeRepo.GetStoreUsers(CurrentStore.Id); + vm.StoreId = CurrentStore.Id; + vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel() + { + Email = u.Email, + Id = u.Id, + Role = u.StoreRole.Role + }).ToList(); } } diff --git a/BTCPayServer/Controllers/UIStoresController.cs b/BTCPayServer/Controllers/UIStoresController.cs index fbb7cf328..ca6dcd5a9 100644 --- a/BTCPayServer/Controllers/UIStoresController.cs +++ b/BTCPayServer/Controllers/UIStoresController.cs @@ -24,141 +24,140 @@ using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.Extensions.Options; using StoreData = BTCPayServer.Data.StoreData; -namespace BTCPayServer.Controllers +namespace BTCPayServer.Controllers; + +[Route("stores")] +[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] +[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] +[AutoValidateAntiforgeryToken] +public partial class UIStoresController : Controller { - [Route("stores")] - [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] - [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - [AutoValidateAntiforgeryToken] - public partial class UIStoresController : Controller + public UIStoresController( + BTCPayServerOptions btcpayServerOptions, + BTCPayServerEnvironment btcpayEnv, + StoreRepository storeRepo, + TokenRepository tokenRepo, + UserManager userManager, + BitpayAccessTokenController tokenController, + BTCPayWalletProvider walletProvider, + BTCPayNetworkProvider networkProvider, + RateFetcher rateFactory, + ExplorerClientProvider explorerProvider, + LanguageService langService, + PaymentMethodHandlerDictionary paymentMethodHandlerDictionary, + PoliciesSettings policiesSettings, + IAuthorizationService authorizationService, + AppService appService, + IFileService fileService, + WebhookSender webhookNotificationManager, + IDataProtectionProvider dataProtector, + IOptions lightningNetworkOptions, + IOptions externalServiceOptions, + IHtmlHelper html, + EmailSenderFactory emailSenderFactory, + WalletFileParsers onChainWalletParsers, + SettingsRepository settingsRepository, + EventAggregator eventAggregator) { - public UIStoresController( - BTCPayServerOptions btcpayServerOptions, - BTCPayServerEnvironment btcpayEnv, - StoreRepository storeRepo, - TokenRepository tokenRepo, - UserManager userManager, - BitpayAccessTokenController tokenController, - BTCPayWalletProvider walletProvider, - BTCPayNetworkProvider networkProvider, - RateFetcher rateFactory, - ExplorerClientProvider explorerProvider, - LanguageService langService, - PaymentMethodHandlerDictionary paymentMethodHandlerDictionary, - PoliciesSettings policiesSettings, - IAuthorizationService authorizationService, - AppService appService, - IFileService fileService, - WebhookSender webhookNotificationManager, - IDataProtectionProvider dataProtector, - IOptions lightningNetworkOptions, - IOptions externalServiceOptions, - IHtmlHelper html, - EmailSenderFactory emailSenderFactory, - WalletFileParsers onChainWalletParsers, - SettingsRepository settingsRepository, - EventAggregator eventAggregator) - { - _rateFactory = rateFactory; - _storeRepo = storeRepo; - _tokenRepository = tokenRepo; - _userManager = userManager; - _langService = langService; - _tokenController = tokenController; - _walletProvider = walletProvider; - _handlers = paymentMethodHandlerDictionary; - _policiesSettings = policiesSettings; - _authorizationService = authorizationService; - _appService = appService; - _fileService = fileService; - _networkProvider = networkProvider; - _explorerProvider = explorerProvider; - _btcpayServerOptions = btcpayServerOptions; - _btcPayEnv = btcpayEnv; - _externalServiceOptions = externalServiceOptions; - _emailSenderFactory = emailSenderFactory; - _onChainWalletParsers = onChainWalletParsers; - _settingsRepository = settingsRepository; - _eventAggregator = eventAggregator; - _html = html; - _dataProtector = dataProtector.CreateProtector("ConfigProtector"); - _webhookNotificationManager = webhookNotificationManager; - _lightningNetworkOptions = lightningNetworkOptions.Value; - } + _rateFactory = rateFactory; + _storeRepo = storeRepo; + _tokenRepository = tokenRepo; + _userManager = userManager; + _langService = langService; + _tokenController = tokenController; + _walletProvider = walletProvider; + _handlers = paymentMethodHandlerDictionary; + _policiesSettings = policiesSettings; + _authorizationService = authorizationService; + _appService = appService; + _fileService = fileService; + _networkProvider = networkProvider; + _explorerProvider = explorerProvider; + _btcpayServerOptions = btcpayServerOptions; + _btcPayEnv = btcpayEnv; + _externalServiceOptions = externalServiceOptions; + _emailSenderFactory = emailSenderFactory; + _onChainWalletParsers = onChainWalletParsers; + _settingsRepository = settingsRepository; + _eventAggregator = eventAggregator; + _html = html; + _dataProtector = dataProtector.CreateProtector("ConfigProtector"); + _webhookNotificationManager = webhookNotificationManager; + _lightningNetworkOptions = lightningNetworkOptions.Value; + } - private readonly BTCPayServerOptions _btcpayServerOptions; - private readonly BTCPayServerEnvironment _btcPayEnv; - private readonly BTCPayNetworkProvider _networkProvider; - private readonly BTCPayWalletProvider _walletProvider; - private readonly BitpayAccessTokenController _tokenController; - private readonly StoreRepository _storeRepo; - private readonly TokenRepository _tokenRepository; - private readonly UserManager _userManager; - private readonly RateFetcher _rateFactory; - private readonly SettingsRepository _settingsRepository; - private readonly ExplorerClientProvider _explorerProvider; - private readonly LanguageService _langService; - private readonly PaymentMethodHandlerDictionary _handlers; - private readonly PoliciesSettings _policiesSettings; - private readonly IAuthorizationService _authorizationService; - private readonly AppService _appService; - private readonly IFileService _fileService; - private readonly IOptions _externalServiceOptions; - private readonly EmailSenderFactory _emailSenderFactory; - private readonly WalletFileParsers _onChainWalletParsers; - private readonly EventAggregator _eventAggregator; - private readonly IHtmlHelper _html; - private readonly WebhookSender _webhookNotificationManager; - private readonly LightningNetworkOptions _lightningNetworkOptions; - private readonly IDataProtector _dataProtector; + private readonly BTCPayServerOptions _btcpayServerOptions; + private readonly BTCPayServerEnvironment _btcPayEnv; + private readonly BTCPayNetworkProvider _networkProvider; + private readonly BTCPayWalletProvider _walletProvider; + private readonly BitpayAccessTokenController _tokenController; + private readonly StoreRepository _storeRepo; + private readonly TokenRepository _tokenRepository; + private readonly UserManager _userManager; + private readonly RateFetcher _rateFactory; + private readonly SettingsRepository _settingsRepository; + private readonly ExplorerClientProvider _explorerProvider; + private readonly LanguageService _langService; + private readonly PaymentMethodHandlerDictionary _handlers; + private readonly PoliciesSettings _policiesSettings; + private readonly IAuthorizationService _authorizationService; + private readonly AppService _appService; + private readonly IFileService _fileService; + private readonly IOptions _externalServiceOptions; + private readonly EmailSenderFactory _emailSenderFactory; + private readonly WalletFileParsers _onChainWalletParsers; + private readonly EventAggregator _eventAggregator; + private readonly IHtmlHelper _html; + private readonly WebhookSender _webhookNotificationManager; + private readonly LightningNetworkOptions _lightningNetworkOptions; + private readonly IDataProtector _dataProtector; - public string? GeneratedPairingCode { get; set; } + public string? GeneratedPairingCode { get; set; } - [TempData] - private bool StoreNotConfigured { get; set; } + [TempData] + private bool StoreNotConfigured { get; set; } - [AllowAnonymous] - [HttpGet("{storeId}/index")] - public async Task Index(string storeId) - { - var userId = _userManager.GetUserId(User); - if (string.IsNullOrEmpty(userId)) - return Forbid(); - - var store = await _storeRepo.FindStore(storeId); - if (store is null) - return NotFound(); - - if ((await _authorizationService.AuthorizeAsync(User, Policies.CanModifyStoreSettings)).Succeeded) - { - return RedirectToAction("Dashboard", new { storeId }); - } - if ((await _authorizationService.AuthorizeAsync(User, Policies.CanViewInvoices)).Succeeded) - { - return RedirectToAction("ListInvoices", "UIInvoice", new { storeId }); - } + [AllowAnonymous] + [HttpGet("{storeId}/index")] + public async Task Index(string storeId) + { + var userId = _userManager.GetUserId(User); + if (string.IsNullOrEmpty(userId)) return Forbid(); + + var store = await _storeRepo.FindStore(storeId); + if (store is null) + return NotFound(); + + if ((await _authorizationService.AuthorizeAsync(User, Policies.CanModifyStoreSettings)).Succeeded) + { + return RedirectToAction("Dashboard", new { storeId }); } + if ((await _authorizationService.AuthorizeAsync(User, Policies.CanViewInvoices)).Succeeded) + { + return RedirectToAction("ListInvoices", "UIInvoice", new { storeId }); + } + return Forbid(); + } - public StoreData CurrentStore => HttpContext.GetStoreData(); + public StoreData CurrentStore => HttpContext.GetStoreData(); - public PaymentMethodOptionViewModel.Format[] GetEnabledPaymentMethodChoices(StoreData storeData) - { - var enabled = storeData.GetEnabledPaymentIds(); + public PaymentMethodOptionViewModel.Format[] GetEnabledPaymentMethodChoices(StoreData storeData) + { + var enabled = storeData.GetEnabledPaymentIds(); - return enabled - .Select(o => - new PaymentMethodOptionViewModel.Format() - { - Name = o.ToString(), - Value = o.ToString(), - PaymentId = o - }).ToArray(); - } + return enabled + .Select(o => + new PaymentMethodOptionViewModel.Format() + { + Name = o.ToString(), + Value = o.ToString(), + PaymentId = o + }).ToArray(); + } - private string? GetUserId() - { - return User.Identity?.AuthenticationType != AuthenticationSchemes.Cookie ? null : _userManager.GetUserId(User); - } + private string? GetUserId() + { + return User.Identity?.AuthenticationType != AuthenticationSchemes.Cookie ? null : _userManager.GetUserId(User); } }