btcpayserver/BTCPayServer/Controllers/StoresController.cs

1039 lines
43 KiB
C#
Raw Normal View History

2020-06-29 04:44:35 +02:00
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
2020-03-19 11:11:15 +01:00
using BTCPayServer.Client;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
2017-09-13 16:50:36 +02:00
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
2019-01-07 09:52:27 +01:00
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
2018-05-03 18:46:52 +02:00
using BTCPayServer.Rating;
2018-04-29 19:33:42 +02:00
using BTCPayServer.Security;
using BTCPayServer.Security.Bitpay;
2018-02-12 19:27:36 +01:00
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
2020-09-18 17:20:31 +02:00
using BundlerMinifier.TagHelpers;
2017-09-13 16:50:36 +02:00
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Options;
2017-09-13 16:50:36 +02:00
using NBitcoin;
2017-12-06 10:08:21 +01:00
using NBitcoin.DataEncoders;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using StoreData = BTCPayServer.Data.StoreData;
2017-09-13 16:50:36 +02:00
namespace BTCPayServer.Controllers
{
[Route("stores")]
2019-10-12 13:35:30 +02:00
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[AutoValidateAntiforgeryToken]
public partial class StoresController : Controller
{
readonly RateFetcher _RateFactory;
2018-03-23 08:24:57 +01:00
public string CreatedStoreId { get; set; }
public StoresController(
IServiceProvider serviceProvider,
BTCPayServerOptions btcpayServerOptions,
BTCPayServerEnvironment btcpayEnv,
StoreRepository repo,
TokenRepository tokenRepo,
UserManager<ApplicationUser> userManager,
AccessTokenController tokenController,
BTCPayWalletProvider walletProvider,
BTCPayNetworkProvider networkProvider,
RateFetcher rateFactory,
ExplorerClientProvider explorerProvider,
2018-03-23 09:27:48 +01:00
LanguageService langService,
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
SettingsRepository settingsRepository,
IAuthorizationService authorizationService,
EventAggregator eventAggregator,
CssThemeManager cssThemeManager,
2020-09-18 17:20:31 +02:00
AppService appService,
2020-11-06 12:42:26 +01:00
IWebHostEnvironment webHostEnvironment,
WebhookNotificationManager webhookNotificationManager,
IOptions<LightningNetworkOptions> lightningNetworkOptions)
{
2018-05-03 18:46:52 +02:00
_RateFactory = rateFactory;
_Repo = repo;
_TokenRepository = tokenRepo;
_UserManager = userManager;
2018-03-23 09:27:48 +01:00
_LangService = langService;
_TokenController = tokenController;
_WalletProvider = walletProvider;
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
_settingsRepository = settingsRepository;
_authorizationService = authorizationService;
_CssThemeManager = cssThemeManager;
_appService = appService;
2020-09-18 17:20:31 +02:00
_webHostEnvironment = webHostEnvironment;
_lightningNetworkOptions = lightningNetworkOptions;
2020-11-06 12:42:26 +01:00
WebhookNotificationManager = webhookNotificationManager;
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
_ExplorerProvider = explorerProvider;
_ServiceProvider = serviceProvider;
_BtcpayServerOptions = btcpayServerOptions;
_BTCPayEnv = btcpayEnv;
}
readonly BTCPayServerOptions _BtcpayServerOptions;
readonly BTCPayServerEnvironment _BTCPayEnv;
readonly IServiceProvider _ServiceProvider;
readonly BTCPayNetworkProvider _NetworkProvider;
private readonly ExplorerClientProvider _ExplorerProvider;
readonly BTCPayWalletProvider _WalletProvider;
readonly AccessTokenController _TokenController;
readonly StoreRepository _Repo;
readonly TokenRepository _TokenRepository;
readonly UserManager<ApplicationUser> _UserManager;
private readonly LanguageService _LangService;
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
private readonly SettingsRepository _settingsRepository;
private readonly IAuthorizationService _authorizationService;
private readonly CssThemeManager _CssThemeManager;
private readonly AppService _appService;
2020-09-18 17:20:31 +02:00
private readonly IWebHostEnvironment _webHostEnvironment;
private readonly IOptions<LightningNetworkOptions> _lightningNetworkOptions;
private readonly EventAggregator _EventAggregator;
[TempData]
public bool StoreNotConfigured
{
get; set;
}
[HttpGet]
2018-03-23 08:24:57 +01:00
[Route("{storeId}/users")]
public async Task<IActionResult> StoreUsers()
2018-03-23 08:24:57 +01:00
{
StoreUsersViewModel vm = new StoreUsersViewModel();
await FillUsers(vm);
2018-03-23 08:24:57 +01:00
return View(vm);
}
private async Task FillUsers(StoreUsersViewModel vm)
{
2019-10-12 13:35:30 +02:00
var users = await _Repo.GetStoreUsers(CurrentStore.Id);
vm.StoreId = CurrentStore.Id;
2018-03-23 08:24:57 +01:00
vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel()
{
2018-03-23 08:24:57 +01:00
Email = u.Email,
Id = u.Id,
Role = u.Role
}).ToList();
}
2019-10-12 13:35:30 +02:00
public StoreData CurrentStore
{
get
{
return this.HttpContext.GetStoreData();
}
}
2018-03-23 08:24:57 +01:00
[HttpPost]
[Route("{storeId}/users")]
public async Task<IActionResult> StoreUsers(StoreUsersViewModel vm)
2018-02-15 05:33:29 +01:00
{
await FillUsers(vm);
2018-03-24 12:40:26 +01:00
if (!ModelState.IsValid)
2018-02-15 05:33:29 +01:00
{
2018-03-23 08:24:57 +01:00
return View(vm);
}
var user = await _UserManager.FindByEmailAsync(vm.Email);
2018-03-24 12:40:26 +01:00
if (user == null)
2018-03-23 08:24:57 +01:00
{
ModelState.AddModelError(nameof(vm.Email), "User not found");
return View(vm);
}
2018-03-24 12:40:26 +01:00
if (!StoreRoles.AllRoles.Contains(vm.Role))
2018-03-23 08:24:57 +01:00
{
ModelState.AddModelError(nameof(vm.Role), "Invalid role");
return View(vm);
}
2019-10-12 13:35:30 +02:00
if (!await _Repo.AddStoreUser(CurrentStore.Id, user.Id, vm.Role))
2018-03-23 08:24:57 +01:00
{
ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store");
return View(vm);
2018-02-15 05:33:29 +01:00
}
TempData[WellKnownTempData.SuccessMessage] = "User added successfully";
2018-03-23 08:24:57 +01:00
return RedirectToAction(nameof(StoreUsers));
2018-02-15 05:33:29 +01:00
}
[HttpGet]
2018-03-23 08:24:57 +01:00
[Route("{storeId}/users/{userId}/delete")]
public async Task<IActionResult> DeleteStoreUser(string userId)
{
2018-03-23 08:24:57 +01:00
StoreUsersViewModel vm = new StoreUsersViewModel();
var user = await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
return View("Confirm", new ConfirmModel()
{
2018-03-23 08:24:57 +01:00
Title = $"Remove store user",
2019-01-21 05:19:01 +01:00
Description = $"Are you sure you want to remove store access for {user.Email}?",
Action = "Delete"
});
}
[HttpPost]
2018-03-23 08:24:57 +01:00
[Route("{storeId}/users/{userId}/delete")]
public async Task<IActionResult> DeleteStoreUserPost(string storeId, string userId)
{
2018-03-23 08:24:57 +01:00
await _Repo.RemoveStoreUser(storeId, userId);
TempData[WellKnownTempData.SuccessMessage] = "User removed successfully";
2018-03-23 08:24:57 +01:00
return RedirectToAction(nameof(StoreUsers), new { storeId = storeId, userId = userId });
}
2018-05-03 18:46:52 +02:00
[HttpGet]
[Route("{storeId}/rates")]
public IActionResult Rates()
2018-05-03 18:46:52 +02:00
{
var exchanges = GetSupportedExchanges();
2019-10-12 13:35:30 +02:00
var storeBlob = CurrentStore.GetStoreBlob();
2018-05-03 18:46:52 +02:00
var vm = new RatesViewModel();
vm.SetExchangeRates(exchanges, storeBlob.PreferredExchange ?? CoinGeckoRateProvider.CoinGeckoName);
vm.Spread = (double)(storeBlob.Spread * 100m);
2019-10-12 13:35:30 +02:00
vm.StoreId = CurrentStore.Id;
2018-05-03 18:46:52 +02:00
vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString();
vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString();
vm.AvailableExchanges = exchanges;
vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString();
2018-05-03 18:46:52 +02:00
vm.ShowScripting = storeBlob.RateScripting;
return View(vm);
}
[HttpPost]
[Route("{storeId}/rates")]
public async Task<IActionResult> Rates(RatesViewModel model, string command = null, string storeId = null, CancellationToken cancellationToken = default)
2018-05-03 18:46:52 +02:00
{
if (command == "scripting-on")
{
2020-06-28 10:55:27 +02:00
return RedirectToAction(nameof(ShowRateRules), new { scripting = true, storeId = model.StoreId });
}
else if (command == "scripting-off")
{
2020-06-28 10:55:27 +02:00
return RedirectToAction(nameof(ShowRateRules), new { scripting = false, storeId = model.StoreId });
}
var exchanges = GetSupportedExchanges();
model.SetExchangeRates(exchanges, model.PreferredExchange);
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)");
}
2018-05-03 18:46:52 +02:00
if (!ModelState.IsValid)
{
return View(model);
}
if (model.PreferredExchange != null)
model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant();
2019-10-12 13:35:30 +02:00
var blob = CurrentStore.GetStoreBlob();
2018-05-03 18:46:52 +02:00
model.DefaultScript = blob.GetDefaultRateRules(_NetworkProvider).ToString();
model.AvailableExchanges = exchanges;
2018-05-03 18:46:52 +02:00
blob.PreferredExchange = model.PreferredExchange;
blob.Spread = (decimal)model.Spread / 100.0m;
blob.DefaultCurrencyPairs = currencyPairs;
2018-05-03 18:46:52 +02:00
if (!model.ShowScripting)
{
if (!exchanges.Any(provider => provider.Id.Equals(model.PreferredExchange, StringComparison.InvariantCultureIgnoreCase)))
2018-05-03 18:46:52 +02:00
{
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<RateRulesErrors>();
var errorString = String.Join(", ", errors.ToArray());
ModelState.AddModelError(nameof(model.Script), $"Parsing error ({errorString})");
return View(model);
}
else
{
blob.RateScript = rules.ToString();
2018-05-04 04:48:03 +02:00
ModelState.Remove(nameof(model.Script));
model.Script = blob.RateScript;
2018-05-03 18:46:52 +02:00
}
}
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<CurrencyPair>();
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);
2018-05-03 18:46:52 +02:00
var testResults = new List<RatesViewModel.TestResultViewModel>();
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)
2018-05-03 18:46:52 +02:00
: testResult.EvaluatedRule
});
}
model.TestRateRules = testResults;
return View(model);
}
else // command == Save
{
2019-10-12 13:35:30 +02:00
if (CurrentStore.SetStoreBlob(blob))
2018-05-03 18:46:52 +02:00
{
2019-10-12 13:35:30 +02:00
await _Repo.UpdateStore(CurrentStore);
TempData[WellKnownTempData.SuccessMessage] = "Rate settings updated";
2018-05-03 18:46:52 +02:00
}
return RedirectToAction(nameof(Rates), new
{
2019-10-12 13:35:30 +02:00
storeId = CurrentStore.Id
2018-05-03 18:46:52 +02:00
});
}
}
[HttpGet]
[Route("{storeId}/rates/confirm")]
public IActionResult ShowRateRules(bool scripting)
{
return View("Confirm", new ConfirmModel()
{
2018-05-04 09:09:43 +02:00
Action = "Continue",
2018-05-03 18:46:52 +02:00
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)"
2018-05-03 18:46:52 +02:00
: "This action will delete your rate script. Are you sure to turn off rate rules scripting?",
ButtonClass = scripting ? "btn-primary" : "btn-danger"
2018-05-03 18:46:52 +02:00
});
}
[HttpPost]
[Route("{storeId}/rates/confirm")]
public async Task<IActionResult> ShowRateRulesPost(bool scripting)
{
2019-10-12 13:35:30 +02:00
var blob = CurrentStore.GetStoreBlob();
2018-05-03 18:46:52 +02:00
blob.RateScripting = scripting;
blob.RateScript = blob.GetDefaultRateRules(_NetworkProvider).ToString();
2019-10-12 13:35:30 +02:00
CurrentStore.SetStoreBlob(blob);
await _Repo.UpdateStore(CurrentStore);
TempData[WellKnownTempData.SuccessMessage] = "Rate rules scripting activated";
2019-10-12 13:35:30 +02:00
return RedirectToAction(nameof(Rates), new { storeId = CurrentStore.Id });
2018-05-03 18:46:52 +02:00
}
[HttpGet]
[Route("{storeId}/checkout")]
public IActionResult CheckoutExperience()
{
2019-10-12 13:35:30 +02:00
var storeBlob = CurrentStore.GetStoreBlob();
var vm = new CheckoutExperienceViewModel();
2019-10-12 13:35:30 +02:00
SetCryptoCurrencies(vm, CurrentStore);
vm.PaymentMethodCriteria = CurrentStore.GetSupportedPaymentMethods(_NetworkProvider).Select(method =>
{
var existing =
storeBlob.PaymentMethodCriteria.SingleOrDefault(criteria =>
criteria.PaymentMethod == method.PaymentId);
if (existing is null)
{
return new PaymentMethodCriteriaViewModel()
{
PaymentMethod = method.PaymentId.ToString(),
Value = ""
};
}
else
{
return new PaymentMethodCriteriaViewModel()
{
PaymentMethod = existing.PaymentMethod.ToString(),
Type = existing.Above
? PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan
: PaymentMethodCriteriaViewModel.CriteriaType.LessThan,
Value = existing.Value?.ToString() ?? ""
};
}
}).ToList();
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
vm.LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints;
vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback;
vm.LazyPaymentMethods = storeBlob.LazyPaymentMethods;
vm.RedirectAutomatically = storeBlob.RedirectAutomatically;
vm.ShowRecommendedFee = storeBlob.ShowRecommendedFee;
vm.RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget;
vm.CustomCSS = storeBlob.CustomCSS;
vm.CustomLogo = storeBlob.CustomLogo;
vm.HtmlTitle = storeBlob.HtmlTitle;
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
return View(vm);
}
void SetCryptoCurrencies(CheckoutExperienceViewModel vm, Data.StoreData storeData)
{
var choices = storeData.GetEnabledPaymentIds(_NetworkProvider)
.Select(o =>
new CheckoutExperienceViewModel.Format()
{
2020-10-16 07:21:37 +02:00
Name = o.ToPrettyString(),
Value = o.ToString(),
PaymentId = o
}).ToArray();
var defaultPaymentId = storeData.GetDefaultPaymentId(_NetworkProvider);
var chosen = choices.FirstOrDefault(c => c.PaymentId == defaultPaymentId);
vm.PaymentMethods = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen?.Value);
vm.DefaultPaymentMethod = chosen?.Value;
}
[HttpPost]
[Route("{storeId}/checkout")]
public async Task<IActionResult> CheckoutExperience(CheckoutExperienceViewModel model)
{
bool needUpdate = false;
2019-10-12 13:35:30 +02:00
var blob = CurrentStore.GetStoreBlob();
var defaultPaymentMethodId = model.DefaultPaymentMethod == null ? null : PaymentMethodId.Parse(model.DefaultPaymentMethod);
2019-10-12 13:35:30 +02:00
if (CurrentStore.GetDefaultPaymentId(_NetworkProvider) != defaultPaymentMethodId)
{
needUpdate = true;
2019-10-12 13:35:30 +02:00
CurrentStore.SetDefaultPaymentId(defaultPaymentMethodId);
}
2019-10-12 13:35:30 +02:00
SetCryptoCurrencies(model, CurrentStore);
model.SetLanguages(_LangService, model.DefaultLang);
model.PaymentMethodCriteria??= new List<PaymentMethodCriteriaViewModel>();
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 var value))
{
model.AddModelError(viewModel => viewModel.PaymentMethodCriteria[index].Value,
$"{methodCriterion.PaymentMethod}: invalid format (1.0 USD)", this);
}
}
}
if (!ModelState.IsValid)
{
return View(model);
}
blob.PaymentMethodCriteria = model.PaymentMethodCriteria
.Where(viewModel => !string.IsNullOrEmpty(viewModel.Value)).Select(viewModel =>
{
CurrencyValue.TryParse(viewModel.Value, out var cv);
2020-10-16 07:21:37 +02:00
return new PaymentMethodCriteria() { Above = viewModel.Type == PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan, Value = cv, PaymentMethod = PaymentMethodId.Parse(viewModel.PaymentMethod) };
}).ToList();
blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.LazyPaymentMethods = model.LazyPaymentMethods;
blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi;
blob.LightningPrivateRouteHints = model.LightningPrivateRouteHints;
blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback;
blob.RedirectAutomatically = model.RedirectAutomatically;
blob.ShowRecommendedFee = model.ShowRecommendedFee;
blob.RecommendedFeeBlockTarget = model.RecommendedFeeBlockTarget;
blob.CustomLogo = model.CustomLogo;
blob.CustomCSS = model.CustomCSS;
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;
blob.DefaultLang = model.DefaultLang;
2019-10-12 13:35:30 +02:00
if (CurrentStore.SetStoreBlob(blob))
{
needUpdate = true;
}
if (needUpdate)
{
2019-10-12 13:35:30 +02:00
await _Repo.UpdateStore(CurrentStore);
TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated";
}
return RedirectToAction(nameof(CheckoutExperience), new
{
2019-10-12 13:35:30 +02:00
storeId = CurrentStore.Id
});
}
2018-07-27 13:37:16 +02:00
private void AddPaymentMethods(StoreData store, StoreBlob storeBlob, StoreViewModel vm)
{
2018-07-27 13:37:16 +02:00
var excludeFilters = storeBlob.GetExcludedPaymentMethods();
2018-03-24 12:40:26 +01:00
var derivationByCryptoCode =
store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationSchemeSettings>()
2019-12-24 08:20:44 +01:00
.ToDictionary(c => c.Network.CryptoCode.ToUpperInvariant());
var lightningByCryptoCode = store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
2019-12-24 08:20:44 +01:00
.ToDictionary(c => c.CryptoCode.ToUpperInvariant());
foreach (var paymentMethodId in _paymentMethodHandlerDictionary.Distinct().SelectMany(handler => handler.GetSupportedPaymentMethods()))
{
2020-06-28 10:55:27 +02:00
switch (paymentMethodId.PaymentType)
{
case BitcoinPaymentType _:
var strategy = derivationByCryptoCode.TryGet(paymentMethodId.CryptoCode);
2019-12-24 08:20:44 +01:00
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
var value = strategy?.ToPrettyString() ?? string.Empty;
2020-06-28 10:55:27 +02:00
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
{
Crypto = paymentMethodId.CryptoCode,
2019-12-24 08:20:44 +01:00
WalletSupported = network.WalletSupported,
Value = value,
WalletId = new WalletId(store.Id, paymentMethodId.CryptoCode),
Enabled = !excludeFilters.Match(paymentMethodId) && strategy != null,
#if ALTCOINS
Collapsed = network is ElementsBTCPayNetwork elementsBTCPayNetwork && elementsBTCPayNetwork.NetworkCryptoCode != elementsBTCPayNetwork.CryptoCode && string.IsNullOrEmpty(value)
2020-07-28 22:48:51 +02:00
#endif
});
break;
case LightningPaymentType _:
var lightning = lightningByCryptoCode.TryGet(paymentMethodId.CryptoCode);
vm.LightningNodes.Add(new StoreViewModel.LightningNode()
{
CryptoCode = paymentMethodId.CryptoCode,
Address = lightning?.GetDisplayableConnectionString(),
Enabled = !excludeFilters.Match(paymentMethodId) && lightning != null
});
break;
2020-06-28 10:55:27 +02:00
}
}
2019-04-04 20:56:12 +02:00
2018-12-11 12:47:38 +01:00
var coinSwitchEnabled = storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled;
vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.AdditionalPaymentMethod()
2018-12-11 12:47:38 +01:00
{
Enabled = coinSwitchEnabled,
Action = nameof(UpdateCoinSwitchSettings),
Provider = "CoinSwitch"
});
}
2020-06-28 10:55:27 +02:00
[HttpGet]
[Route("{storeId}")]
public IActionResult UpdateStore()
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var storeBlob = store.GetStoreBlob();
var vm = new StoreViewModel();
vm.Id = store.Id;
vm.StoreName = store.StoreName;
vm.StoreWebsite = store.StoreWebsite;
vm.NetworkFeeMode = storeBlob.NetworkFeeMode;
vm.AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice;
vm.SpeedPolicy = store.SpeedPolicy;
vm.CanDelete = _Repo.CanDeleteStores();
AddPaymentMethods(store, storeBlob, vm);
vm.MonitoringExpiration = (int)storeBlob.MonitoringExpiration.TotalMinutes;
vm.InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes;
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
vm.PaymentTolerance = storeBlob.PaymentTolerance;
vm.PayJoinEnabled = storeBlob.PayJoinEnabled;
vm.HintWallet = storeBlob.Hints.Wallet;
2020-10-16 07:21:37 +02:00
vm.HintLightning = storeBlob.Hints.Lightning;
return View(vm);
}
[HttpPost]
[Route("{storeId}")]
public async Task<IActionResult> UpdateStore(StoreViewModel model, string command = null)
{
bool needUpdate = false;
2019-10-12 13:35:30 +02:00
if (CurrentStore.SpeedPolicy != model.SpeedPolicy)
{
needUpdate = true;
2019-10-12 13:35:30 +02:00
CurrentStore.SpeedPolicy = model.SpeedPolicy;
}
2019-10-12 13:35:30 +02:00
if (CurrentStore.StoreName != model.StoreName)
{
needUpdate = true;
2019-10-12 13:35:30 +02:00
CurrentStore.StoreName = model.StoreName;
}
2019-10-12 13:35:30 +02:00
if (CurrentStore.StoreWebsite != model.StoreWebsite)
{
needUpdate = true;
2019-10-12 13:35:30 +02:00
CurrentStore.StoreWebsite = model.StoreWebsite;
}
2019-10-12 13:35:30 +02:00
var blob = CurrentStore.GetStoreBlob();
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
blob.NetworkFeeMode = model.NetworkFeeMode;
blob.MonitoringExpiration = TimeSpan.FromMinutes(model.MonitoringExpiration);
blob.InvoiceExpiration = TimeSpan.FromMinutes(model.InvoiceExpiration);
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
blob.PaymentTolerance = model.PaymentTolerance;
var payjoinChanged = blob.PayJoinEnabled != model.PayJoinEnabled;
blob.PayJoinEnabled = model.PayJoinEnabled;
2019-10-12 13:35:30 +02:00
if (CurrentStore.SetStoreBlob(blob))
{
needUpdate = true;
}
if (needUpdate)
{
2019-10-12 13:35:30 +02:00
await _Repo.UpdateStore(CurrentStore);
2020-06-28 10:55:27 +02:00
TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated";
if (payjoinChanged && blob.PayJoinEnabled)
{
var problematicPayjoinEnabledMethods = CurrentStore.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationSchemeSettings>()
.Where(settings =>
settings.Network.SupportPayJoin &&
string.IsNullOrEmpty(_ExplorerProvider.GetExplorerClient(settings.Network)
.GetMetadata<string>(settings.AccountDerivation,
WellknownMetadataKeys.Mnemonic)))
.Select(settings => settings.PaymentId.CryptoCode)
.ToArray();
if (problematicPayjoinEnabledMethods.Any())
{
TempData.Remove(WellKnownTempData.SuccessMessage);
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Warning,
2020-06-11 16:09:33 +02:00
Html = $"The store was updated successfully. However, payjoin will not work for {string.Join(", ", problematicPayjoinEnabledMethods)} until you configure them to be a <a href='https://docs.btcpayserver.org/HotWallet/' class='alert-link' target='_blank'>hot wallet</a>."
});
}
}
}
return RedirectToAction(nameof(UpdateStore), new
{
2019-10-12 13:35:30 +02:00
storeId = CurrentStore.Id
});
}
[HttpGet]
[Route("{storeId}/delete")]
public IActionResult DeleteStore(string storeId)
{
return View("Confirm", new ConfirmModel()
{
2019-10-31 07:10:00 +01:00
Action = "Delete",
Title = "Delete this store",
Description = "This action is irreversible and will remove all information related to this store. (Invoices, Apps etc...)",
ButtonClass = "btn-danger"
});
}
[HttpPost]
[Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStorePost(string storeId)
{
2019-10-12 13:35:30 +02:00
await _Repo.DeleteStore(CurrentStore.Id);
TempData[WellKnownTempData.SuccessMessage] = "Store successfully deleted";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
private IEnumerable<AvailableRateProvider> GetSupportedExchanges()
{
var exchanges = _RateFactory.RateProviderFactory.GetSupportedExchanges();
return exchanges
.Where(r => !string.IsNullOrWhiteSpace(r.Name))
.OrderBy(s => s.Id, StringComparer.OrdinalIgnoreCase);
}
private DerivationSchemeSettings ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
{
2019-05-09 09:05:18 +02:00
var parser = new DerivationSchemeParser(network);
2018-03-24 12:40:26 +01:00
parser.HintScriptPubKey = hint;
try
{
var derivationSchemeSettings = new DerivationSchemeSettings();
derivationSchemeSettings.Network = network;
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;
}
catch (Exception)
{
// ignored
}
return new DerivationSchemeSettings(parser.Parse(derivationScheme), network);
}
[HttpGet]
[Route("{storeId}/Tokens")]
public async Task<IActionResult> ListTokens()
{
var model = new TokensViewModel();
2019-10-12 13:35:30 +02:00
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();
2019-10-12 13:35:30 +02:00
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);
}
2018-10-31 09:59:09 +01:00
[HttpGet]
[Route("{storeId}/tokens/{tokenId}/revoke")]
public async Task<IActionResult> RevokeToken(string tokenId)
{
var token = await _TokenRepository.GetToken(tokenId);
2019-10-12 13:35:30 +02:00
if (token == null || token.StoreId != CurrentStore.Id)
2018-10-31 09:59:09 +01:00
return NotFound();
return View("Confirm", new ConfirmModel()
{
2019-10-31 07:10:00 +01:00
Action = "Revoke",
2018-10-31 09:59:09 +01:00
Title = "Revoke the token",
Description = $"The access token with the label \"{token.Label}\" will be revoked, do you wish to continue?",
ButtonClass = "btn-danger"
});
}
[HttpPost]
[Route("{storeId}/tokens/{tokenId}/revoke")]
public async Task<IActionResult> RevokeTokenConfirm(string tokenId)
{
var token = await _TokenRepository.GetToken(tokenId);
if (token == null ||
2019-10-12 13:35:30 +02:00
token.StoreId != CurrentStore.Id ||
2018-10-31 09:59:09 +01:00
!await _TokenRepository.DeleteToken(tokenId))
TempData[WellKnownTempData.ErrorMessage] = "Failure to revoke this token";
2018-10-31 09:59:09 +01:00
else
TempData[WellKnownTempData.SuccessMessage] = "Token revoked";
2020-06-28 10:55:27 +02:00
return RedirectToAction(nameof(ListTokens), new { storeId = token.StoreId });
2018-10-31 09:59:09 +01:00
}
[HttpGet]
[Route("{storeId}/tokens/{tokenId}")]
public async Task<IActionResult> ShowToken(string tokenId)
{
var token = await _TokenRepository.GetToken(tokenId);
2019-10-12 13:35:30 +02:00
if (token == null || token.StoreId != CurrentStore.Id)
2018-10-31 09:59:09 +01:00
return NotFound();
return View(token);
}
[HttpPost]
[Route("{storeId}/Tokens/Create")]
public async Task<IActionResult> CreateToken(string storeId, CreateTokenViewModel model)
{
if (!ModelState.IsValid)
{
return View(nameof(CreateToken), model);
}
model.Label = model.Label ?? String.Empty;
2018-03-23 08:24:57 +01:00
var userId = GetUserId();
if (userId == null)
2019-10-12 13:35:30 +02:00
return Challenge(AuthenticationSchemes.Cookie);
storeId = model.StoreId;
var store = 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))
};
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, storeId);
pairingCode = tokenRequest.PairingCode;
}
else
{
pairingCode = (await _TokenController.Tokens(tokenRequest)).Data[0].PairingCode;
}
2018-01-09 18:07:42 +01:00
GeneratedPairingCode = pairingCode;
return RedirectToAction(nameof(RequestPairing), new
{
pairingCode = pairingCode,
selectedStore = storeId
});
}
2018-01-09 18:07:42 +01:00
public string GeneratedPairingCode { get; set; }
2020-11-06 12:42:26 +01:00
public WebhookNotificationManager WebhookNotificationManager { get; }
2018-01-09 18:07:42 +01:00
[HttpGet]
[Route("{storeId}/Tokens/Create")]
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);
}
[HttpGet]
[Route("/api-tokens")]
[AllowAnonymous]
public async Task<IActionResult> CreateToken()
{
var userId = GetUserId();
if (string.IsNullOrWhiteSpace(userId))
2019-10-12 13:35:30 +02:00
return Challenge(AuthenticationSchemes.Cookie);
var storeId = CurrentStore?.Id;
var model = new CreateTokenViewModel();
ViewBag.HidePublicKey = true;
ViewBag.ShowStores = true;
ViewBag.ShowMenu = false;
var stores = await _Repo.GetStoresByUserId(userId);
model.Stores = new SelectList(stores.Where(s => s.Role == StoreRoles.Owner), nameof(CurrentStore.Id), nameof(CurrentStore.StoreName));
2020-01-12 07:32:26 +01:00
if (!model.Stores.Any())
{
TempData[WellKnownTempData.ErrorMessage] = "You need to be owner of at least one store before pairing";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
return View(model);
}
[HttpPost]
[Route("/api-tokens")]
[AllowAnonymous]
public Task<IActionResult> CreateToken2(CreateTokenViewModel model)
{
return CreateToken(model.StoreId, model);
}
[HttpPost]
[Route("{storeId}/tokens/apikey")]
2020-06-28 10:55:27 +02:00
public async Task<IActionResult> GenerateAPIKey(string storeId, string command = "")
{
2018-04-29 19:33:42 +02:00
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";
}
2020-06-28 10:55:27 +02:00
return RedirectToAction(nameof(ListTokens), new
{
storeId
});
}
[HttpGet]
[Route("/api-access-request")]
[AllowAnonymous]
public async Task<IActionResult> RequestPairing(string pairingCode, string selectedStore = null)
{
var userId = GetUserId();
if (userId == null)
2019-10-12 13:35:30 +02:00
return Challenge(AuthenticationSchemes.Cookie);
2018-03-23 08:24:57 +01:00
if (pairingCode == null)
return NotFound();
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
if (pairing == null)
{
TempData[WellKnownTempData.ErrorMessage] = "Unknown pairing code";
2018-03-23 08:24:57 +01:00
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
else
{
var stores = await _Repo.GetStoresByUserId(userId);
return View(new PairingModel()
{
Id = pairing.Id,
Label = pairing.Label,
SIN = pairing.SIN ?? "Server-Initiated Pairing",
2019-10-12 13:35:30 +02:00
StoreId = selectedStore ?? stores.FirstOrDefault()?.Id,
Stores = stores.Where(u => u.Role == StoreRoles.Owner).Select(s => new PairingModel.StoreViewModel()
{
Id = s.Id,
Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName
}).ToArray()
});
}
}
[HttpPost]
2018-03-23 08:24:57 +01:00
[Route("/api-access-request")]
2019-10-12 13:35:30 +02:00
public async Task<IActionResult> Pair(string pairingCode, string storeId)
{
if (pairingCode == null)
return NotFound();
2019-10-12 13:35:30 +02:00
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();
2020-01-12 07:32:26 +01:00
StoreNotConfigured = !store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(p => !excludeFilter.Match(p.PaymentId))
2020-01-12 07:32:26 +01:00
.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()
{
2019-10-12 13:35:30 +02:00
if (User.Identity.AuthenticationType != AuthenticationSchemes.Cookie)
return null;
return _UserManager.GetUserId(User);
}
// TODO: Need to have talk about how architect default currency implementation
// For now we have also hardcoded USD for Store creation and then Invoice creation
const string DEFAULT_CURRENCY = "USD";
[Route("{storeId}/paybutton")]
public async Task<IActionResult> PayButton()
{
2019-10-12 13:35:30 +02:00
var store = CurrentStore;
var storeBlob = store.GetStoreBlob();
if (!storeBlob.AnyoneCanInvoice)
{
return View("PayButtonEnable", null);
}
var apps = await _appService.GetAllApps(_UserManager.GetUserId(User), false, store.Id);
2018-08-23 04:11:39 +02:00
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash();
var model = new PayButtonViewModel
{
Price = 10,
Currency = DEFAULT_CURRENCY,
ButtonSize = 2,
UrlRoot = appUrl,
PayButtonImageUrl = appUrl + "img/paybutton/pay.svg",
StoreId = store.Id,
2019-04-04 21:32:16 +02:00
ButtonType = 0,
2019-04-04 20:56:12 +02:00
Min = 1,
Max = 20,
Step = 1,
Apps = apps
};
return View(model);
}
[HttpPost]
[Route("{storeId}/paybutton")]
public async Task<IActionResult> PayButton(bool enableStore)
{
2019-10-12 13:35:30 +02:00
var blob = CurrentStore.GetStoreBlob();
blob.AnyoneCanInvoice = enableStore;
2019-10-12 13:35:30 +02:00
if (CurrentStore.SetStoreBlob(blob))
{
2019-10-12 13:35:30 +02:00
await _Repo.UpdateStore(CurrentStore);
TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated";
}
return RedirectToAction(nameof(PayButton), new
{
2019-10-12 13:35:30 +02:00
storeId = CurrentStore.Id
});
}
}
2017-09-13 16:50:36 +02:00
}