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.Authentication; using BTCPayServer.Configuration; using BTCPayServer.Data; using BTCPayServer.Models; using BTCPayServer.Models.AppViewModels; using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Payments; using BTCPayServer.Payments.Changelly; using BTCPayServer.Payments.Lightning; using BTCPayServer.Rating; using BTCPayServer.Security; using BTCPayServer.Services; using BTCPayServer.Services.Rates; using BTCPayServer.Services.Stores; using BTCPayServer.Services.Wallets; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.Extensions.Options; using NBitcoin; using NBitcoin.DataEncoders; namespace BTCPayServer.Controllers { [Route("stores")] [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] [Authorize(Policy = Policies.CanModifyStoreSettings.Key)] [AutoValidateAntiforgeryToken] public partial class StoresController : Controller { RateFetcher _RateFactory; public string CreatedStoreId { get; set; } public StoresController( IServiceProvider serviceProvider, BTCPayServerOptions btcpayServerOptions, BTCPayServerEnvironment btcpayEnv, StoreRepository repo, TokenRepository tokenRepo, UserManager userManager, AccessTokenController tokenController, BTCPayWalletProvider walletProvider, BTCPayNetworkProvider networkProvider, RateFetcher rateFactory, ExplorerClientProvider explorerProvider, IFeeProviderFactory feeRateProvider, LanguageService langService, ChangellyClientProvider changellyClientProvider, IOptions mvcJsonOptions, IHostingEnvironment env, IHttpClientFactory httpClientFactory) { _RateFactory = rateFactory; _Repo = repo; _TokenRepository = tokenRepo; _UserManager = userManager; _LangService = langService; _changellyClientProvider = changellyClientProvider; MvcJsonOptions = mvcJsonOptions; _TokenController = tokenController; _WalletProvider = walletProvider; _Env = env; _httpClientFactory = httpClientFactory; _NetworkProvider = networkProvider; _ExplorerProvider = explorerProvider; _FeeRateProvider = feeRateProvider; _ServiceProvider = serviceProvider; _BtcpayServerOptions = btcpayServerOptions; _BTCPayEnv = btcpayEnv; } BTCPayServerOptions _BtcpayServerOptions; BTCPayServerEnvironment _BTCPayEnv; IServiceProvider _ServiceProvider; BTCPayNetworkProvider _NetworkProvider; private ExplorerClientProvider _ExplorerProvider; private IFeeProviderFactory _FeeRateProvider; BTCPayWalletProvider _WalletProvider; AccessTokenController _TokenController; StoreRepository _Repo; TokenRepository _TokenRepository; UserManager _UserManager; private LanguageService _LangService; private readonly ChangellyClientProvider _changellyClientProvider; IHostingEnvironment _Env; private IHttpClientFactory _httpClientFactory; [TempData] public string StatusMessage { get; set; } [TempData] public bool StoreNotConfigured { get; set; } [HttpGet] [Route("{storeId}/users")] public async Task StoreUsers() { StoreUsersViewModel vm = new StoreUsersViewModel(); await FillUsers(vm); return View(vm); } private async Task FillUsers(StoreUsersViewModel vm) { var users = await _Repo.GetStoreUsers(StoreData.Id); vm.StoreId = StoreData.Id; vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel() { Email = u.Email, Id = u.Id, Role = u.Role }).ToList(); } public StoreData StoreData { get { return this.HttpContext.GetStoreData(); } } [HttpPost] [Route("{storeId}/users")] public async Task StoreUsers(StoreUsersViewModel vm) { await FillUsers(vm); if (!ModelState.IsValid) { return View(vm); } var user = await _UserManager.FindByEmailAsync(vm.Email); if (user == null) { ModelState.AddModelError(nameof(vm.Email), "User not found"); return View(vm); } if (!StoreRoles.AllRoles.Contains(vm.Role)) { ModelState.AddModelError(nameof(vm.Role), "Invalid role"); return View(vm); } if (!await _Repo.AddStoreUser(StoreData.Id, user.Id, vm.Role)) { ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store"); return View(vm); } StatusMessage = "User added successfully"; return RedirectToAction(nameof(StoreUsers)); } [HttpGet] [Route("{storeId}/users/{userId}/delete")] public async Task DeleteStoreUser(string userId) { StoreUsersViewModel vm = new StoreUsersViewModel(); var user = await _UserManager.FindByIdAsync(userId); if (user == null) return NotFound(); return View("Confirm", new ConfirmModel() { Title = $"Remove store user", Description = $"Are you sure you want to remove store access for {user.Email}?", Action = "Delete" }); } [HttpPost] [Route("{storeId}/users/{userId}/delete")] public async Task DeleteStoreUserPost(string storeId, string userId) { await _Repo.RemoveStoreUser(storeId, userId); StatusMessage = "User removed successfully"; return RedirectToAction(nameof(StoreUsers), new { storeId = storeId, userId = userId }); } [HttpGet] [Route("{storeId}/rates")] public IActionResult Rates(string storeId) { var storeBlob = StoreData.GetStoreBlob(); var vm = new RatesViewModel(); vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName); vm.Spread = (double)(storeBlob.Spread * 100m); vm.StoreId = storeId; vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString(); vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString(); vm.AvailableExchanges = GetSupportedExchanges(); vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString(); vm.ShowScripting = storeBlob.RateScripting; return View(vm); } [HttpPost] [Route("{storeId}/rates")] public async Task Rates(RatesViewModel model, string command = null, string storeId = null, CancellationToken cancellationToken = default) { model.SetExchangeRates(GetSupportedExchanges(), 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)"); } if (!ModelState.IsValid) { return View(model); } if (model.PreferredExchange != null) model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant(); var blob = StoreData.GetStoreBlob(); model.DefaultScript = blob.GetDefaultRateRules(_NetworkProvider).ToString(); model.AvailableExchanges = GetSupportedExchanges(); blob.PreferredExchange = model.PreferredExchange; blob.Spread = (decimal)model.Spread / 100.0m; blob.DefaultCurrencyPairs = currencyPairs; if (!model.ShowScripting) { if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase)) { 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 (StoreData.SetStoreBlob(blob)) { await _Repo.UpdateStore(StoreData); StatusMessage = "Rate settings updated"; } return RedirectToAction(nameof(Rates), new { storeId = StoreData.Id }); } } [HttpGet] [Route("{storeId}/rates/confirm")] 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 = "btn-primary" }); } [HttpPost] [Route("{storeId}/rates/confirm")] public async Task ShowRateRulesPost(bool scripting) { var blob = StoreData.GetStoreBlob(); blob.RateScripting = scripting; blob.RateScript = blob.GetDefaultRateRules(_NetworkProvider).ToString(); StoreData.SetStoreBlob(blob); await _Repo.UpdateStore(StoreData); StatusMessage = "Rate rules scripting activated"; return RedirectToAction(nameof(Rates), new { storeId = StoreData.Id }); } [HttpGet] [Route("{storeId}/checkout")] public IActionResult CheckoutExperience() { var storeBlob = StoreData.GetStoreBlob(); var vm = new CheckoutExperienceViewModel(); SetCryptoCurrencies(vm, StoreData); vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri; vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri; vm.HtmlTitle = storeBlob.HtmlTitle; vm.SetLanguages(_LangService, storeBlob.DefaultLang); vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail; vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? ""; vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? ""; vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi; vm.RedirectAutomatically = storeBlob.RedirectAutomatically; return View(vm); } void SetCryptoCurrencies(CheckoutExperienceViewModel vm, Data.StoreData storeData) { var choices = storeData.GetEnabledPaymentIds(_NetworkProvider) .Select(o => new CheckoutExperienceViewModel.Format() { Name = GetDisplayName(o), Value = o.ToString(), PaymentId = o }).ToArray(); var defaultPaymentId = storeData.GetDefaultPaymentId(_NetworkProvider); var chosen = choices.FirstOrDefault(c => c.PaymentId == defaultPaymentId); vm.CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen?.Value); vm.DefaultPaymentMethod = chosen?.Value; } private string GetDisplayName(PaymentMethodId paymentMethodId) { var display = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode)?.DisplayName ?? paymentMethodId.CryptoCode; return paymentMethodId.PaymentType == PaymentTypes.BTCLike ? display : $"{display} (Lightning)"; } [HttpPost] [Route("{storeId}/checkout")] public async Task CheckoutExperience(CheckoutExperienceViewModel model) { CurrencyValue lightningMaxValue = null; if (!string.IsNullOrWhiteSpace(model.LightningMaxValue)) { if (!CurrencyValue.TryParse(model.LightningMaxValue, out lightningMaxValue)) { ModelState.AddModelError(nameof(model.LightningMaxValue), "Invalid lightning max value"); } } CurrencyValue onchainMinValue = null; if (!string.IsNullOrWhiteSpace(model.OnChainMinValue)) { if (!CurrencyValue.TryParse(model.OnChainMinValue, out onchainMinValue)) { ModelState.AddModelError(nameof(model.OnChainMinValue), "Invalid on chain min value"); } } bool needUpdate = false; var blob = StoreData.GetStoreBlob(); var defaultPaymentMethodId = model.DefaultPaymentMethod == null ? null : PaymentMethodId.Parse(model.DefaultPaymentMethod); if (StoreData.GetDefaultPaymentId(_NetworkProvider) != defaultPaymentMethodId) { needUpdate = true; StoreData.SetDefaultPaymentId(defaultPaymentMethodId); } SetCryptoCurrencies(model, StoreData); model.SetLanguages(_LangService, model.DefaultLang); if (!ModelState.IsValid) { return View(model); } blob.CustomLogo = string.IsNullOrWhiteSpace(model.CustomLogo) ? null : new Uri(model.CustomLogo, UriKind.Absolute); blob.CustomCSS = string.IsNullOrWhiteSpace(model.CustomCSS) ? null : new Uri(model.CustomCSS, UriKind.Absolute); blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle; blob.DefaultLang = model.DefaultLang; blob.RequiresRefundEmail = model.RequiresRefundEmail; blob.OnChainMinValue = onchainMinValue; blob.LightningMaxValue = lightningMaxValue; blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi; blob.RedirectAutomatically = model.RedirectAutomatically; if (StoreData.SetStoreBlob(blob)) { needUpdate = true; } if (needUpdate) { await _Repo.UpdateStore(StoreData); StatusMessage = "Store successfully updated"; } return RedirectToAction(nameof(CheckoutExperience), new { storeId = StoreData.Id }); } [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 = storeBlob.MonitoringExpiration; vm.InvoiceExpiration = storeBlob.InvoiceExpiration; vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate; vm.PaymentTolerance = storeBlob.PaymentTolerance; return View(vm); } private void AddPaymentMethods(StoreData store, StoreBlob storeBlob, StoreViewModel vm) { var excludeFilters = storeBlob.GetExcludedPaymentMethods(); var derivationByCryptoCode = store .GetSupportedPaymentMethods(_NetworkProvider) .OfType() .ToDictionary(c => c.Network.CryptoCode); foreach (var network in _NetworkProvider.GetAll()) { var strategy = derivationByCryptoCode.TryGet(network.CryptoCode); vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme() { Crypto = network.CryptoCode, Value = strategy?.ToPrettyString() ?? string.Empty, WalletId = new WalletId(store.Id, network.CryptoCode), Enabled = !excludeFilters.Match(new Payments.PaymentMethodId(network.CryptoCode, Payments.PaymentTypes.BTCLike)) }); } var lightningByCryptoCode = store .GetSupportedPaymentMethods(_NetworkProvider) .OfType() .ToDictionary(c => c.CryptoCode); foreach (var network in _NetworkProvider.GetAll()) { var lightning = lightningByCryptoCode.TryGet(network.CryptoCode); var paymentId = new Payments.PaymentMethodId(network.CryptoCode, Payments.PaymentTypes.LightningLike); vm.LightningNodes.Add(new StoreViewModel.LightningNode() { CryptoCode = network.CryptoCode, Address = lightning?.GetLightningUrl()?.BaseUri.AbsoluteUri ?? string.Empty, Enabled = !excludeFilters.Match(paymentId) }); } var changellyEnabled = storeBlob.ChangellySettings != null && storeBlob.ChangellySettings.Enabled; vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.ThirdPartyPaymentMethod() { Enabled = changellyEnabled, Action = nameof(UpdateChangellySettings), Provider = "Changelly" }); var coinSwitchEnabled = storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled; vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.ThirdPartyPaymentMethod() { Enabled = coinSwitchEnabled, Action = nameof(UpdateCoinSwitchSettings), Provider = "CoinSwitch" }); } [HttpPost] [Route("{storeId}")] public async Task UpdateStore(StoreViewModel model, string command = null) { bool needUpdate = false; if (StoreData.SpeedPolicy != model.SpeedPolicy) { needUpdate = true; StoreData.SpeedPolicy = model.SpeedPolicy; } if (StoreData.StoreName != model.StoreName) { needUpdate = true; StoreData.StoreName = model.StoreName; } if (StoreData.StoreWebsite != model.StoreWebsite) { needUpdate = true; StoreData.StoreWebsite = model.StoreWebsite; } var blob = StoreData.GetStoreBlob(); blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice; blob.NetworkFeeMode = model.NetworkFeeMode; blob.MonitoringExpiration = model.MonitoringExpiration; blob.InvoiceExpiration = model.InvoiceExpiration; blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty; blob.PaymentTolerance = model.PaymentTolerance; if (StoreData.SetStoreBlob(blob)) { needUpdate = true; } if (needUpdate) { await _Repo.UpdateStore(StoreData); StatusMessage = "Store successfully updated"; } return RedirectToAction(nameof(UpdateStore), new { storeId = StoreData.Id }); } [HttpGet] [Route("{storeId}/delete")] public IActionResult DeleteStore(string storeId) { return View("Confirm", new ConfirmModel() { Action = "Delete this store", 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 DeleteStorePost(string storeId) { await _Repo.DeleteStore(StoreData.Id); StatusMessage = "Success: Store successfully deleted"; return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores"); } private CoinAverageExchange[] GetSupportedExchanges() { return _RateFactory.RateProviderFactory.GetSupportedExchanges() .Select(c => c.Value) .OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase) .ToArray(); } private DerivationSchemeSettings ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network) { var parser = new DerivationSchemeParser(network); parser.HintScriptPubKey = hint; return new DerivationSchemeSettings(parser.Parse(derivationScheme), network); } [HttpGet] [Route("{storeId}/Tokens")] public async Task ListTokens() { var model = new TokensViewModel(); var tokens = await _TokenRepository.GetTokensByStoreIdAsync(StoreData.Id); model.StatusMessage = StatusMessage; 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(StoreData.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] [Route("{storeId}/tokens/{tokenId}/revoke")] public async Task RevokeToken(string tokenId) { var token = await _TokenRepository.GetToken(tokenId); if (token == null || token.StoreId != StoreData.Id) return NotFound(); return View("Confirm", new ConfirmModel() { Action = "Revoke the token", 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 RevokeTokenConfirm(string tokenId) { var token = await _TokenRepository.GetToken(tokenId); if (token == null || token.StoreId != StoreData.Id || !await _TokenRepository.DeleteToken(tokenId)) StatusMessage = "Failure to revoke this token"; else StatusMessage = "Token revoked"; return RedirectToAction(nameof(ListTokens)); } [HttpGet] [Route("{storeId}/tokens/{tokenId}")] public async Task ShowToken(string tokenId) { var token = await _TokenRepository.GetToken(tokenId); if (token == null || token.StoreId != StoreData.Id) return NotFound(); return View(token); } [HttpPost] [Route("/api-tokens")] [Route("{storeId}/Tokens/Create")] [AllowAnonymous] public async Task CreateToken(CreateTokenViewModel model) { if (!ModelState.IsValid) { return View(model); } model.Label = model.Label ?? String.Empty; var userId = GetUserId(); if (userId == null) return Challenge(Policies.CookieAuthentication); var store = StoreData; var storeId = StoreData?.Id; if (storeId == null) { storeId = model.StoreId; store = await _Repo.FindStore(storeId, userId); if (store == null) return Challenge(Policies.CookieAuthentication); } if (!store.HasClaim(Policies.CanModifyStoreSettings.Key)) { return Challenge(Policies.CookieAuthentication); } 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 = ((DataWrapper>)await _TokenController.Tokens(tokenRequest)).Data[0].PairingCode; } GeneratedPairingCode = pairingCode; return RedirectToAction(nameof(RequestPairing), new { pairingCode = pairingCode, selectedStore = storeId }); } public string GeneratedPairingCode { get; set; } public IOptions MvcJsonOptions { get; } [HttpGet] [Route("/api-tokens")] [Route("{storeId}/Tokens/Create")] [AllowAnonymous] public async Task CreateToken() { var userId = GetUserId(); if (string.IsNullOrWhiteSpace(userId)) return Challenge(Policies.CookieAuthentication); var storeId = StoreData?.Id; if (StoreData != null) { if (!StoreData.HasClaim(Policies.CanModifyStoreSettings.Key)) { return Challenge(Policies.CookieAuthentication); } } var model = new CreateTokenViewModel(); ViewBag.HidePublicKey = storeId == null; ViewBag.ShowStores = storeId == null; ViewBag.ShowMenu = storeId != null; model.StoreId = storeId; if (storeId == null) { var stores = await _Repo.GetStoresByUserId(userId); model.Stores = new SelectList(stores.Where(s => s.HasClaim(Policies.CanModifyStoreSettings.Key)), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId); if (model.Stores.Count() == 0) { StatusMessage = "Error: You need to be owner of at least one store before pairing"; return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores"); } } return View(model); } [HttpPost] [Route("{storeId}/tokens/apikey")] public async Task GenerateAPIKey() { var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); await _TokenRepository.GenerateLegacyAPIKey(StoreData.Id); StatusMessage = "API Key re-generated"; return RedirectToAction(nameof(ListTokens)); } [HttpGet] [Route("/api-access-request")] [AllowAnonymous] public async Task RequestPairing(string pairingCode, string selectedStore = null) { var userId = GetUserId(); if (userId == null) return Challenge(Policies.CookieAuthentication); if (pairingCode == null) return NotFound(); var pairing = await _TokenRepository.GetPairingAsync(pairingCode); if (pairing == null) { StatusMessage = "Unknown pairing code"; 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", SelectedStore = selectedStore ?? stores.FirstOrDefault()?.Id, Stores = stores.Where(u => u.HasClaim(Policies.CanModifyStoreSettings.Key)).Select(s => new PairingModel.StoreViewModel() { Id = s.Id, Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName }).ToArray() }); } } [HttpPost] [Route("/api-access-request")] [AllowAnonymous] public async Task Pair(string pairingCode, string selectedStore) { if (pairingCode == null) return NotFound(); var userId = GetUserId(); if (userId == null) return Challenge(Policies.CookieAuthentication); var store = await _Repo.FindStore(selectedStore, userId); var pairing = await _TokenRepository.GetPairingAsync(pairingCode); if (store == null || pairing == null) return NotFound(); if (!store.HasClaim(Policies.CanModifyStoreSettings.Key)) { return Challenge(Policies.CookieAuthentication); } var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id); if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial) { var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods(); StoreNotConfigured = store.GetSupportedPaymentMethods(_NetworkProvider) .Where(p => !excludeFilter.Match(p.PaymentId)) .Count() == 0; StatusMessage = "Pairing is successful"; if (pairingResult == PairingResult.Partial) StatusMessage = "Server initiated pairing code: " + pairingCode; return RedirectToAction(nameof(ListTokens), new { storeId = store.Id, pairingCode = pairingCode }); } else { StatusMessage = $"Pairing failed ({pairingResult})"; return RedirectToAction(nameof(ListTokens), new { storeId = store.Id }); } } private string GetUserId() { if (User.Identity.AuthenticationType != Policies.CookieAuthentication) 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 IActionResult PayButton() { var store = StoreData; var storeBlob = store.GetStoreBlob(); if (!storeBlob.AnyoneCanInvoice) { return View("PayButtonEnable", null); } var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash(); var model = new PayButtonViewModel { Price = 10, Currency = DEFAULT_CURRENCY, ButtonSize = 2, UrlRoot = appUrl, PayButtonImageUrl = appUrl + "img/paybutton/pay.png", StoreId = store.Id, ButtonType = 0, Min = 1, Max = 20, Step = 1 }; return View(model); } [HttpPost] [Route("{storeId}/paybutton")] public async Task PayButton(bool enableStore) { var blob = StoreData.GetStoreBlob(); blob.AnyoneCanInvoice = enableStore; if (StoreData.SetStoreBlob(blob)) { await _Repo.UpdateStore(StoreData); StatusMessage = "Store successfully updated"; } return RedirectToAction(nameof(PayButton), new { storeId = StoreData.Id }); } } }