From 71ba5d9c4c2d095794fd33f05c09c3891369d9dc Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Thu, 4 Apr 2024 10:47:28 +0200 Subject: [PATCH] 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)