mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-21 14:04:12 +01:00
Convert to file-scoped namespace
This commit is contained in:
parent
c7a8523b77
commit
620ebc751c
11 changed files with 2590 additions and 2601 deletions
|
@ -16,156 +16,155 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
namespace BTCPayServer.Controllers;
|
||||
|
||||
public partial class UIStoresController
|
||||
{
|
||||
public partial class UIStoresController
|
||||
[HttpGet("{storeId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Dashboard()
|
||||
{
|
||||
[HttpGet("{storeId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Dashboard()
|
||||
{
|
||||
var store = CurrentStore;
|
||||
if (store is null)
|
||||
return NotFound();
|
||||
var store = CurrentStore;
|
||||
if (store is null)
|
||||
return NotFound();
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
|
||||
AddPaymentMethods(store, storeBlob,
|
||||
out var derivationSchemes, out var lightningNodes);
|
||||
AddPaymentMethods(store, storeBlob,
|
||||
out var derivationSchemes, out var lightningNodes);
|
||||
|
||||
var walletEnabled = derivationSchemes.Any(scheme => !string.IsNullOrEmpty(scheme.Value) && scheme.Enabled);
|
||||
var lightningEnabled = lightningNodes.Any(ln => !string.IsNullOrEmpty(ln.Address) && ln.Enabled);
|
||||
var cryptoCode = _networkProvider.DefaultNetwork.CryptoCode;
|
||||
var vm = new StoreDashboardViewModel
|
||||
{
|
||||
WalletEnabled = walletEnabled,
|
||||
LightningEnabled = lightningEnabled,
|
||||
LightningSupported = _networkProvider.GetNetwork<BTCPayNetwork>(cryptoCode)?.SupportLightning is true,
|
||||
StoreId = CurrentStore.Id,
|
||||
StoreName = CurrentStore.StoreName,
|
||||
CryptoCode = cryptoCode,
|
||||
Network = _networkProvider.DefaultNetwork,
|
||||
IsSetUp = walletEnabled || lightningEnabled
|
||||
};
|
||||
|
||||
// Widget data
|
||||
if (vm is { WalletEnabled: false, LightningEnabled: false })
|
||||
return View(vm);
|
||||
|
||||
var userId = GetUserId();
|
||||
if (userId is null)
|
||||
return NotFound();
|
||||
|
||||
var apps = await _appService.GetAllApps(userId, false, store.Id);
|
||||
foreach (var app in apps)
|
||||
{
|
||||
var appData = await _appService.GetAppData(userId, app.Id);
|
||||
vm.Apps.Add(appData);
|
||||
}
|
||||
var walletEnabled = derivationSchemes.Any(scheme => !string.IsNullOrEmpty(scheme.Value) && scheme.Enabled);
|
||||
var lightningEnabled = lightningNodes.Any(ln => !string.IsNullOrEmpty(ln.Address) && ln.Enabled);
|
||||
var cryptoCode = _networkProvider.DefaultNetwork.CryptoCode;
|
||||
var vm = new StoreDashboardViewModel
|
||||
{
|
||||
WalletEnabled = walletEnabled,
|
||||
LightningEnabled = lightningEnabled,
|
||||
LightningSupported = _networkProvider.GetNetwork<BTCPayNetwork>(cryptoCode)?.SupportLightning is true,
|
||||
StoreId = CurrentStore.Id,
|
||||
StoreName = CurrentStore.StoreName,
|
||||
CryptoCode = cryptoCode,
|
||||
Network = _networkProvider.DefaultNetwork,
|
||||
IsSetUp = walletEnabled || lightningEnabled
|
||||
};
|
||||
|
||||
// Widget data
|
||||
if (vm is { WalletEnabled: false, LightningEnabled: false })
|
||||
return View(vm);
|
||||
|
||||
var userId = GetUserId();
|
||||
if (userId is null)
|
||||
return NotFound();
|
||||
|
||||
var apps = await _appService.GetAllApps(userId, false, store.Id);
|
||||
foreach (var app in apps)
|
||||
{
|
||||
var appData = await _appService.GetAppData(userId, app.Id);
|
||||
vm.Apps.Add(appData);
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/dashboard/{cryptoCode}/lightning/balance")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult LightningBalance(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var vm = new StoreLightningBalanceViewModel { Store = store, CryptoCode = cryptoCode };
|
||||
return ViewComponent("StoreLightningBalance", new { vm });
|
||||
}
|
||||
[HttpGet("{storeId}/dashboard/{cryptoCode}/lightning/balance")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult LightningBalance(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
[HttpGet("{storeId}/dashboard/{cryptoCode}/numbers")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult StoreNumbers(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var vm = new StoreLightningBalanceViewModel { Store = store, CryptoCode = cryptoCode };
|
||||
return ViewComponent("StoreLightningBalance", new { vm });
|
||||
}
|
||||
|
||||
var vm = new StoreNumbersViewModel { Store = store, CryptoCode = cryptoCode };
|
||||
return ViewComponent("StoreNumbers", new { vm });
|
||||
}
|
||||
[HttpGet("{storeId}/dashboard/{cryptoCode}/numbers")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult StoreNumbers(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
[HttpGet("{storeId}/dashboard/{cryptoCode}/recent-transactions")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult RecentTransactions(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var vm = new StoreNumbersViewModel { Store = store, CryptoCode = cryptoCode };
|
||||
return ViewComponent("StoreNumbers", new { vm });
|
||||
}
|
||||
|
||||
var vm = new StoreRecentTransactionsViewModel { Store = store, CryptoCode = cryptoCode };
|
||||
return ViewComponent("StoreRecentTransactions", new { vm });
|
||||
}
|
||||
[HttpGet("{storeId}/dashboard/{cryptoCode}/recent-transactions")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult RecentTransactions(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
[HttpGet("{storeId}/dashboard/{cryptoCode}/recent-invoices")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult RecentInvoices(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var vm = new StoreRecentTransactionsViewModel { Store = store, CryptoCode = cryptoCode };
|
||||
return ViewComponent("StoreRecentTransactions", new { vm });
|
||||
}
|
||||
|
||||
var vm = new StoreRecentInvoicesViewModel { Store = store, CryptoCode = cryptoCode };
|
||||
return ViewComponent("StoreRecentInvoices", new { vm });
|
||||
}
|
||||
[HttpGet("{storeId}/dashboard/{cryptoCode}/recent-invoices")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult RecentInvoices(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
internal void AddPaymentMethods(StoreData store, StoreBlob storeBlob,
|
||||
out List<StoreDerivationScheme> derivationSchemes, out List<StoreLightningNode> lightningNodes)
|
||||
{
|
||||
var excludeFilters = storeBlob.GetExcludedPaymentMethods();
|
||||
var derivationByCryptoCode =
|
||||
store
|
||||
var vm = new StoreRecentInvoicesViewModel { Store = store, CryptoCode = cryptoCode };
|
||||
return ViewComponent("StoreRecentInvoices", new { vm });
|
||||
}
|
||||
|
||||
internal void AddPaymentMethods(StoreData store, StoreBlob storeBlob,
|
||||
out List<StoreDerivationScheme> derivationSchemes, out List<StoreLightningNode> lightningNodes)
|
||||
{
|
||||
var excludeFilters = storeBlob.GetExcludedPaymentMethods();
|
||||
var derivationByCryptoCode =
|
||||
store
|
||||
.GetPaymentMethodConfigs<DerivationSchemeSettings>(_handlers)
|
||||
.ToDictionary(c => ((IHasNetwork)_handlers[c.Key]).Network.CryptoCode, c => c.Value);
|
||||
|
||||
var lightningByCryptoCode = store
|
||||
.GetPaymentMethodConfigs(_handlers)
|
||||
.Where(c => c.Value is LightningPaymentMethodConfig)
|
||||
.ToDictionary(c => ((IHasNetwork)_handlers[c.Key]).Network.CryptoCode, c => (LightningPaymentMethodConfig)c.Value);
|
||||
var lightningByCryptoCode = store
|
||||
.GetPaymentMethodConfigs(_handlers)
|
||||
.Where(c => c.Value is LightningPaymentMethodConfig)
|
||||
.ToDictionary(c => ((IHasNetwork)_handlers[c.Key]).Network.CryptoCode, c => (LightningPaymentMethodConfig)c.Value);
|
||||
|
||||
derivationSchemes = [];
|
||||
lightningNodes = [];
|
||||
derivationSchemes = [];
|
||||
lightningNodes = [];
|
||||
|
||||
foreach (var handler in _handlers)
|
||||
foreach (var handler in _handlers)
|
||||
{
|
||||
if (handler is BitcoinLikePaymentHandler { Network: var network })
|
||||
{
|
||||
if (handler is BitcoinLikePaymentHandler { Network: var network })
|
||||
{
|
||||
var strategy = derivationByCryptoCode.TryGet(network.CryptoCode);
|
||||
var value = strategy?.ToPrettyString() ?? string.Empty;
|
||||
var strategy = derivationByCryptoCode.TryGet(network.CryptoCode);
|
||||
var value = strategy?.ToPrettyString() ?? string.Empty;
|
||||
|
||||
derivationSchemes.Add(new StoreDerivationScheme
|
||||
{
|
||||
Crypto = network.CryptoCode,
|
||||
PaymentMethodId = handler.PaymentMethodId,
|
||||
WalletSupported = network.WalletSupported,
|
||||
Value = value,
|
||||
WalletId = new WalletId(store.Id, network.CryptoCode),
|
||||
Enabled = !excludeFilters.Match(handler.PaymentMethodId) && strategy != null,
|
||||
derivationSchemes.Add(new StoreDerivationScheme
|
||||
{
|
||||
Crypto = network.CryptoCode,
|
||||
PaymentMethodId = handler.PaymentMethodId,
|
||||
WalletSupported = network.WalletSupported,
|
||||
Value = value,
|
||||
WalletId = new WalletId(store.Id, network.CryptoCode),
|
||||
Enabled = !excludeFilters.Match(handler.PaymentMethodId) && strategy != null,
|
||||
#if ALTCOINS
|
||||
Collapsed = network is Plugins.Altcoins.ElementsBTCPayNetwork elementsBTCPayNetwork && elementsBTCPayNetwork.NetworkCryptoCode != elementsBTCPayNetwork.CryptoCode && string.IsNullOrEmpty(value)
|
||||
#endif
|
||||
});
|
||||
}
|
||||
else if (handler is LightningLikePaymentHandler)
|
||||
});
|
||||
}
|
||||
else if (handler is LightningLikePaymentHandler)
|
||||
{
|
||||
var lnNetwork = ((IHasNetwork)handler).Network;
|
||||
var lightning = lightningByCryptoCode.TryGet(lnNetwork.CryptoCode);
|
||||
var isEnabled = !excludeFilters.Match(handler.PaymentMethodId) && lightning != null;
|
||||
lightningNodes.Add(new StoreLightningNode
|
||||
{
|
||||
var lnNetwork = ((IHasNetwork)handler).Network;
|
||||
var lightning = lightningByCryptoCode.TryGet(lnNetwork.CryptoCode);
|
||||
var isEnabled = !excludeFilters.Match(handler.PaymentMethodId) && lightning != null;
|
||||
lightningNodes.Add(new StoreLightningNode
|
||||
{
|
||||
CryptoCode = lnNetwork.CryptoCode,
|
||||
PaymentMethodId = handler.PaymentMethodId,
|
||||
Address = lightning?.GetDisplayableConnectionString(),
|
||||
Enabled = isEnabled
|
||||
});
|
||||
}
|
||||
CryptoCode = lnNetwork.CryptoCode,
|
||||
PaymentMethodId = handler.PaymentMethodId,
|
||||
Address = lightning?.GetDisplayableConnectionString(),
|
||||
Enabled = isEnabled
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,255 +15,254 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using MimeKit;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
namespace BTCPayServer.Controllers;
|
||||
|
||||
public partial class UIStoresController
|
||||
{
|
||||
public partial class UIStoresController
|
||||
[HttpGet("{storeId}/emails")]
|
||||
public async Task<IActionResult> StoreEmails(string storeId)
|
||||
{
|
||||
[HttpGet("{storeId}/emails")]
|
||||
public async Task<IActionResult> StoreEmails(string storeId)
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
if (blob.EmailSettings?.IsComplete() is not true && !TempData.HasStatusMessage())
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
if (blob.EmailSettings?.IsComplete() is not true && !TempData.HasStatusMessage())
|
||||
{
|
||||
var emailSender = await _emailSenderFactory.GetEmailSender(store.Id) as StoreEmailSender;
|
||||
if (!await IsSetupComplete(emailSender?.FallbackSender))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Warning,
|
||||
Html = $"You need to configure email settings before this feature works. <a class='alert-link' href='{Url.Action("StoreEmailSettings", new { storeId })}'>Configure store email settings</a>."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var vm = new StoreEmailRuleViewModel { Rules = blob.EmailRules ?? [] };
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/emails")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> StoreEmails(string storeId, StoreEmailRuleViewModel vm, string command)
|
||||
{
|
||||
vm.Rules ??= [];
|
||||
int commandIndex = 0;
|
||||
|
||||
var indSep = command.Split(':', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (indSep.Length > 1)
|
||||
{
|
||||
commandIndex = int.Parse(indSep[1], CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (command.StartsWith("remove", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
vm.Rules.RemoveAt(commandIndex);
|
||||
}
|
||||
else if (command == "add")
|
||||
{
|
||||
vm.Rules.Add(new StoreEmailRule());
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
for (var i = 0; i < vm.Rules.Count; i++)
|
||||
{
|
||||
var rule = vm.Rules[i];
|
||||
|
||||
if (!string.IsNullOrEmpty(rule.To) && rule.To.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Any(s => !MailboxAddressValidator.TryParse(s, out _)))
|
||||
{
|
||||
ModelState.AddModelError($"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}",
|
||||
"Invalid mailbox address provided. Valid formats are: 'test@example.com' or 'Firstname Lastname <test@example.com>'");
|
||||
}
|
||||
else if (!rule.CustomerEmail && string.IsNullOrEmpty(rule.To))
|
||||
ModelState.AddModelError($"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}",
|
||||
"Either recipient or \"Send the email to the buyer\" is required");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var store = HttpContext.GetStoreData();
|
||||
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
string message = "";
|
||||
|
||||
// update rules
|
||||
var blob = store.GetStoreBlob();
|
||||
blob.EmailRules = vm.Rules;
|
||||
if (store.SetStoreBlob(blob))
|
||||
{
|
||||
await _storeRepo.UpdateStore(store);
|
||||
message += "Store email rules saved. ";
|
||||
}
|
||||
|
||||
if (command.StartsWith("test", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
var rule = vm.Rules[commandIndex];
|
||||
var emailSender = await _emailSenderFactory.GetEmailSender(store.Id);
|
||||
if (await IsSetupComplete(emailSender))
|
||||
{
|
||||
var recipients = rule.To.Split(",", StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(o =>
|
||||
{
|
||||
MailboxAddressValidator.TryParse(o, out var mb);
|
||||
return mb;
|
||||
})
|
||||
.Where(o => o != null)
|
||||
.ToArray();
|
||||
|
||||
emailSender.SendEmail(recipients.ToArray(), null, null, $"[TEST] {rule.Subject}", rule.Body);
|
||||
message += "Test email sent — please verify you received it.";
|
||||
}
|
||||
else
|
||||
{
|
||||
message += "Complete the email setup to send test emails.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = message + "Error sending test email: " + ex.Message;
|
||||
return RedirectToAction("StoreEmails", new { storeId });
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(message))
|
||||
var emailSender = await _emailSenderFactory.GetEmailSender(store.Id) as StoreEmailSender;
|
||||
if (!await IsSetupComplete(emailSender?.FallbackSender))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = message
|
||||
Severity = StatusMessageModel.StatusSeverity.Warning,
|
||||
Html = $"You need to configure email settings before this feature works. <a class='alert-link' href='{Url.Action("StoreEmailSettings", new { storeId })}'>Configure store email settings</a>."
|
||||
});
|
||||
}
|
||||
|
||||
return RedirectToAction("StoreEmails", new { storeId });
|
||||
}
|
||||
|
||||
public class StoreEmailRuleViewModel
|
||||
var vm = new StoreEmailRuleViewModel { Rules = blob.EmailRules ?? [] };
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/emails")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> StoreEmails(string storeId, StoreEmailRuleViewModel vm, string command)
|
||||
{
|
||||
vm.Rules ??= [];
|
||||
int commandIndex = 0;
|
||||
|
||||
var indSep = command.Split(':', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (indSep.Length > 1)
|
||||
{
|
||||
public List<StoreEmailRule> Rules { get; set; }
|
||||
commandIndex = int.Parse(indSep[1], CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public class StoreEmailRule
|
||||
if (command.StartsWith("remove", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
[Required]
|
||||
public string Trigger { get; set; }
|
||||
|
||||
public bool CustomerEmail { get; set; }
|
||||
|
||||
|
||||
public string To { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Subject { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Body { get; set; }
|
||||
vm.Rules.RemoveAt(commandIndex);
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/email-settings")]
|
||||
public async Task<IActionResult> StoreEmailSettings(string storeId)
|
||||
else if (command == "add")
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
vm.Rules.Add(new StoreEmailRule());
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
var data = blob.EmailSettings ?? new EmailSettings();
|
||||
var fallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender
|
||||
? await storeSender.FallbackSender.GetEmailSettings()
|
||||
: null;
|
||||
var vm = new EmailsViewModel(data, fallbackSettings);
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/email-settings")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> StoreEmailSettings(string storeId, EmailsViewModel model, string command, [FromForm] bool useCustomSMTP = false)
|
||||
for (var i = 0; i < vm.Rules.Count; i++)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var rule = vm.Rules[i];
|
||||
|
||||
ViewBag.UseCustomSMTP = useCustomSMTP;
|
||||
model.FallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender
|
||||
? await storeSender.FallbackSender.GetEmailSettings()
|
||||
: null;
|
||||
if (useCustomSMTP)
|
||||
if (!string.IsNullOrEmpty(rule.To) && rule.To.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Any(s => !MailboxAddressValidator.TryParse(s, out _)))
|
||||
{
|
||||
model.Settings.Validate("Settings.", ModelState);
|
||||
ModelState.AddModelError($"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}",
|
||||
"Invalid mailbox address provided. Valid formats are: 'test@example.com' or 'Firstname Lastname <test@example.com>'");
|
||||
}
|
||||
if (command == "Test")
|
||||
else if (!rule.CustomerEmail && string.IsNullOrEmpty(rule.To))
|
||||
ModelState.AddModelError($"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}",
|
||||
"Either recipient or \"Send the email to the buyer\" is required");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var store = HttpContext.GetStoreData();
|
||||
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
string message = "";
|
||||
|
||||
// update rules
|
||||
var blob = store.GetStoreBlob();
|
||||
blob.EmailRules = vm.Rules;
|
||||
if (store.SetStoreBlob(blob))
|
||||
{
|
||||
await _storeRepo.UpdateStore(store);
|
||||
message += "Store email rules saved. ";
|
||||
}
|
||||
|
||||
if (command.StartsWith("test", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
var rule = vm.Rules[commandIndex];
|
||||
var emailSender = await _emailSenderFactory.GetEmailSender(store.Id);
|
||||
if (await IsSetupComplete(emailSender))
|
||||
{
|
||||
if (useCustomSMTP)
|
||||
{
|
||||
if (model.PasswordSet)
|
||||
var recipients = rule.To.Split(",", StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(o =>
|
||||
{
|
||||
model.Settings.Password = store.GetStoreBlob().EmailSettings.Password;
|
||||
}
|
||||
MailboxAddressValidator.TryParse(o, out var mb);
|
||||
return mb;
|
||||
})
|
||||
.Where(o => o != null)
|
||||
.ToArray();
|
||||
|
||||
emailSender.SendEmail(recipients.ToArray(), null, null, $"[TEST] {rule.Subject}", rule.Body);
|
||||
message += "Test email sent — please verify you received it.";
|
||||
}
|
||||
else
|
||||
{
|
||||
message += "Complete the email setup to send test emails.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = message + "Error sending test email: " + ex.Message;
|
||||
return RedirectToAction("StoreEmails", new { storeId });
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(message))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = message
|
||||
});
|
||||
}
|
||||
|
||||
return RedirectToAction("StoreEmails", new { storeId });
|
||||
}
|
||||
|
||||
public class StoreEmailRuleViewModel
|
||||
{
|
||||
public List<StoreEmailRule> Rules { get; set; }
|
||||
}
|
||||
|
||||
public class StoreEmailRule
|
||||
{
|
||||
[Required]
|
||||
public string Trigger { get; set; }
|
||||
|
||||
public bool CustomerEmail { get; set; }
|
||||
|
||||
|
||||
public string To { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Subject { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Body { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/email-settings")]
|
||||
public async Task<IActionResult> StoreEmailSettings(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
var data = blob.EmailSettings ?? new EmailSettings();
|
||||
var fallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender
|
||||
? await storeSender.FallbackSender.GetEmailSettings()
|
||||
: null;
|
||||
var vm = new EmailsViewModel(data, fallbackSettings);
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/email-settings")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> StoreEmailSettings(string storeId, EmailsViewModel model, string command, [FromForm] bool useCustomSMTP = false)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
ViewBag.UseCustomSMTP = useCustomSMTP;
|
||||
model.FallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender
|
||||
? await storeSender.FallbackSender.GetEmailSettings()
|
||||
: null;
|
||||
if (useCustomSMTP)
|
||||
{
|
||||
model.Settings.Validate("Settings.", ModelState);
|
||||
}
|
||||
if (command == "Test")
|
||||
{
|
||||
try
|
||||
{
|
||||
if (useCustomSMTP)
|
||||
{
|
||||
if (model.PasswordSet)
|
||||
{
|
||||
model.Settings.Password = store.GetStoreBlob().EmailSettings.Password;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(model.TestEmail))
|
||||
ModelState.AddModelError(nameof(model.TestEmail), new RequiredAttribute().FormatErrorMessage(nameof(model.TestEmail)));
|
||||
if (!ModelState.IsValid)
|
||||
return View(model);
|
||||
var settings = useCustomSMTP ? model.Settings : model.FallbackSettings;
|
||||
using var client = await settings.CreateSmtpClient();
|
||||
var message = settings.CreateMailMessage(MailboxAddress.Parse(model.TestEmail), $"{store.StoreName}: Email test", "You received it, the BTCPay Server SMTP settings work.", false);
|
||||
await client.SendAsync(message);
|
||||
await client.DisconnectAsync(true);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Email sent to {model.TestEmail}. Please verify you received it.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Error: " + ex.Message;
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
if (command == "ResetPassword")
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.EmailSettings.Password = null;
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _storeRepo.UpdateStore(store);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Email server password reset";
|
||||
}
|
||||
if (useCustomSMTP)
|
||||
{
|
||||
if (model.Settings.From is not null && !MailboxAddressValidator.IsMailboxAddress(model.Settings.From))
|
||||
{
|
||||
ModelState.AddModelError("Settings.From", "Invalid email");
|
||||
}
|
||||
if (string.IsNullOrEmpty(model.TestEmail))
|
||||
ModelState.AddModelError(nameof(model.TestEmail), new RequiredAttribute().FormatErrorMessage(nameof(model.TestEmail)));
|
||||
if (!ModelState.IsValid)
|
||||
return View(model);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
if (storeBlob.EmailSettings != null && new EmailsViewModel(storeBlob.EmailSettings, model.FallbackSettings).PasswordSet)
|
||||
{
|
||||
model.Settings.Password = storeBlob.EmailSettings.Password;
|
||||
}
|
||||
storeBlob.EmailSettings = model.Settings;
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _storeRepo.UpdateStore(store);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Email settings modified";
|
||||
var settings = useCustomSMTP ? model.Settings : model.FallbackSettings;
|
||||
using var client = await settings.CreateSmtpClient();
|
||||
var message = settings.CreateMailMessage(MailboxAddress.Parse(model.TestEmail), $"{store.StoreName}: Email test", "You received it, the BTCPay Server SMTP settings work.", false);
|
||||
await client.SendAsync(message);
|
||||
await client.DisconnectAsync(true);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Email sent to {model.TestEmail}. Please verify you received it.";
|
||||
}
|
||||
return RedirectToAction(nameof(StoreEmailSettings), new { storeId });
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Error: " + ex.Message;
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private static async Task<bool> IsSetupComplete(IEmailSender emailSender)
|
||||
if (command == "ResetPassword")
|
||||
{
|
||||
return emailSender is not null && (await emailSender.GetEmailSettings())?.IsComplete() == true;
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.EmailSettings.Password = null;
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _storeRepo.UpdateStore(store);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Email server password reset";
|
||||
}
|
||||
if (useCustomSMTP)
|
||||
{
|
||||
if (model.Settings.From is not null && !MailboxAddressValidator.IsMailboxAddress(model.Settings.From))
|
||||
{
|
||||
ModelState.AddModelError("Settings.From", "Invalid email");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return View(model);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
if (storeBlob.EmailSettings != null && new EmailsViewModel(storeBlob.EmailSettings, model.FallbackSettings).PasswordSet)
|
||||
{
|
||||
model.Settings.Password = storeBlob.EmailSettings.Password;
|
||||
}
|
||||
storeBlob.EmailSettings = model.Settings;
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _storeRepo.UpdateStore(store);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Email settings modified";
|
||||
}
|
||||
return RedirectToAction(nameof(StoreEmailSettings), new { storeId });
|
||||
}
|
||||
|
||||
private static async Task<bool> IsSetupComplete(IEmailSender emailSender)
|
||||
{
|
||||
return emailSender is not null && (await emailSender.GetEmailSettings())?.IsComplete() == true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,22 +13,22 @@ using Microsoft.AspNetCore.Mvc;
|
|||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class UIStoresController
|
||||
{
|
||||
private async Task<Data.WebhookDeliveryData?> LastDeliveryForWebhook(string webhookId)
|
||||
{
|
||||
return (await _storeRepo.GetWebhookDeliveries(CurrentStore.Id, webhookId, 1)).ToList().FirstOrDefault();
|
||||
}
|
||||
namespace BTCPayServer.Controllers;
|
||||
|
||||
[HttpGet("{storeId}/webhooks")]
|
||||
public async Task<IActionResult> Webhooks()
|
||||
public partial class UIStoresController
|
||||
{
|
||||
private async Task<Data.WebhookDeliveryData?> LastDeliveryForWebhook(string webhookId)
|
||||
{
|
||||
return (await _storeRepo.GetWebhookDeliveries(CurrentStore.Id, webhookId, 1)).ToList().FirstOrDefault();
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks")]
|
||||
public async Task<IActionResult> Webhooks()
|
||||
{
|
||||
var webhooks = await _storeRepo.GetWebhooks(CurrentStore.Id);
|
||||
return View(nameof(Webhooks), new WebhooksViewModel
|
||||
{
|
||||
var webhooks = await _storeRepo.GetWebhooks(CurrentStore.Id);
|
||||
return View(nameof(Webhooks), new WebhooksViewModel
|
||||
{
|
||||
Webhooks = webhooks.Select(async w =>
|
||||
Webhooks = webhooks.Select(async w =>
|
||||
{
|
||||
var lastDelivery = await LastDeliveryForWebhook(w.Id);
|
||||
var lastDeliveryBlob = lastDelivery?.GetBlob();
|
||||
|
@ -42,150 +42,149 @@ namespace BTCPayServer.Controllers
|
|||
LastDeliverySuccessful = lastDeliveryBlob == null ? true : lastDeliveryBlob.Status == WebhookDeliveryStatus.HttpSuccess,
|
||||
};
|
||||
}
|
||||
).Select(t => t.Result).ToArray()
|
||||
).Select(t => t.Result).ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks/new")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult NewWebhook()
|
||||
{
|
||||
return View(nameof(ModifyWebhook), new EditWebhookViewModel
|
||||
{
|
||||
Active = true,
|
||||
Everything = true,
|
||||
IsNew = true,
|
||||
Secret = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20))
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks/{webhookId}/remove")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> DeleteWebhook(string webhookId)
|
||||
{
|
||||
var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
if (webhook is null)
|
||||
return NotFound();
|
||||
|
||||
return View("Confirm", new ConfirmModel("Delete webhook", "This webhook will be removed from this store. Are you sure?", "Delete"));
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/{webhookId}/remove")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> DeleteWebhookPost(string webhookId)
|
||||
{
|
||||
var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
if (webhook is null)
|
||||
return NotFound();
|
||||
|
||||
await _storeRepo.DeleteWebhook(CurrentStore.Id, webhookId);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Webhook successfully deleted";
|
||||
return RedirectToAction(nameof(Webhooks), new { storeId = CurrentStore.Id });
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/new")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> NewWebhook(string storeId, EditWebhookViewModel viewModel)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return View(nameof(ModifyWebhook), viewModel);
|
||||
|
||||
await _storeRepo.CreateWebhook(CurrentStore.Id, viewModel.CreateBlob());
|
||||
TempData[WellKnownTempData.SuccessMessage] = "The webhook has been created";
|
||||
return RedirectToAction(nameof(Webhooks), new { storeId });
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks/{webhookId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ModifyWebhook(string webhookId)
|
||||
{
|
||||
var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
if (webhook is null)
|
||||
return NotFound();
|
||||
|
||||
var blob = webhook.GetBlob();
|
||||
var deliveries = await _storeRepo.GetWebhookDeliveries(CurrentStore.Id, webhookId, 20);
|
||||
return View(nameof(ModifyWebhook), new EditWebhookViewModel(blob)
|
||||
{
|
||||
Deliveries = deliveries
|
||||
.Select(s => new DeliveryViewModel(s)).ToList()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/{webhookId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ModifyWebhook(string webhookId, EditWebhookViewModel viewModel)
|
||||
{
|
||||
var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
if (webhook is null)
|
||||
return NotFound();
|
||||
if (!ModelState.IsValid)
|
||||
return View(nameof(ModifyWebhook), viewModel);
|
||||
|
||||
await _storeRepo.UpdateWebhook(CurrentStore.Id, webhookId, viewModel.CreateBlob());
|
||||
TempData[WellKnownTempData.SuccessMessage] = "The webhook has been updated";
|
||||
return RedirectToAction(nameof(Webhooks), new { storeId = CurrentStore.Id });
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks/{webhookId}/test")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> TestWebhook(string webhookId)
|
||||
{
|
||||
var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
if (webhook is null)
|
||||
return NotFound();
|
||||
|
||||
return View(nameof(TestWebhook));
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/{webhookId}/test")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> TestWebhook(string webhookId, TestWebhookViewModel viewModel, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _webhookNotificationManager.TestWebhook(CurrentStore.Id, webhookId, viewModel.Type, cancellationToken);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"{viewModel.Type} event delivered successfully! Delivery ID is {result.DeliveryId}";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"{viewModel.Type} event could not be delivered. Error message received: {(result.ErrorMessage ?? "unknown")}";
|
||||
}
|
||||
|
||||
return View(nameof(TestWebhook));
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> RedeliverWebhook(string webhookId, string deliveryId)
|
||||
{
|
||||
var delivery = await _storeRepo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId);
|
||||
if (delivery is null)
|
||||
return NotFound();
|
||||
|
||||
var newDeliveryId = await _webhookNotificationManager.Redeliver(deliveryId);
|
||||
if (newDeliveryId is null)
|
||||
return NotFound();
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Successfully planned a redelivery";
|
||||
return RedirectToAction(nameof(ModifyWebhook),
|
||||
new
|
||||
{
|
||||
storeId = CurrentStore.Id,
|
||||
webhookId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks/new")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult NewWebhook()
|
||||
{
|
||||
return View(nameof(ModifyWebhook), new EditWebhookViewModel
|
||||
{
|
||||
Active = true,
|
||||
Everything = true,
|
||||
IsNew = true,
|
||||
Secret = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20))
|
||||
});
|
||||
}
|
||||
[HttpGet("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> WebhookDelivery(string webhookId, string deliveryId)
|
||||
{
|
||||
var delivery = await _storeRepo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId);
|
||||
if (delivery is null)
|
||||
return NotFound();
|
||||
|
||||
[HttpGet("{storeId}/webhooks/{webhookId}/remove")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> DeleteWebhook(string webhookId)
|
||||
{
|
||||
var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
if (webhook is null)
|
||||
return NotFound();
|
||||
|
||||
return View("Confirm", new ConfirmModel("Delete webhook", "This webhook will be removed from this store. Are you sure?", "Delete"));
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/{webhookId}/remove")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> DeleteWebhookPost(string webhookId)
|
||||
{
|
||||
var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
if (webhook is null)
|
||||
return NotFound();
|
||||
|
||||
await _storeRepo.DeleteWebhook(CurrentStore.Id, webhookId);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Webhook successfully deleted";
|
||||
return RedirectToAction(nameof(Webhooks), new { storeId = CurrentStore.Id });
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/new")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> NewWebhook(string storeId, EditWebhookViewModel viewModel)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return View(nameof(ModifyWebhook), viewModel);
|
||||
|
||||
await _storeRepo.CreateWebhook(CurrentStore.Id, viewModel.CreateBlob());
|
||||
TempData[WellKnownTempData.SuccessMessage] = "The webhook has been created";
|
||||
return RedirectToAction(nameof(Webhooks), new { storeId });
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks/{webhookId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ModifyWebhook(string webhookId)
|
||||
{
|
||||
var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
if (webhook is null)
|
||||
return NotFound();
|
||||
|
||||
var blob = webhook.GetBlob();
|
||||
var deliveries = await _storeRepo.GetWebhookDeliveries(CurrentStore.Id, webhookId, 20);
|
||||
return View(nameof(ModifyWebhook), new EditWebhookViewModel(blob)
|
||||
{
|
||||
Deliveries = deliveries
|
||||
.Select(s => new DeliveryViewModel(s)).ToList()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/{webhookId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ModifyWebhook(string webhookId, EditWebhookViewModel viewModel)
|
||||
{
|
||||
var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
if (webhook is null)
|
||||
return NotFound();
|
||||
if (!ModelState.IsValid)
|
||||
return View(nameof(ModifyWebhook), viewModel);
|
||||
|
||||
await _storeRepo.UpdateWebhook(CurrentStore.Id, webhookId, viewModel.CreateBlob());
|
||||
TempData[WellKnownTempData.SuccessMessage] = "The webhook has been updated";
|
||||
return RedirectToAction(nameof(Webhooks), new { storeId = CurrentStore.Id });
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks/{webhookId}/test")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> TestWebhook(string webhookId)
|
||||
{
|
||||
var webhook = await _storeRepo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
if (webhook is null)
|
||||
return NotFound();
|
||||
|
||||
return View(nameof(TestWebhook));
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/{webhookId}/test")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> TestWebhook(string webhookId, TestWebhookViewModel viewModel, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _webhookNotificationManager.TestWebhook(CurrentStore.Id, webhookId, viewModel.Type, cancellationToken);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"{viewModel.Type} event delivered successfully! Delivery ID is {result.DeliveryId}";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"{viewModel.Type} event could not be delivered. Error message received: {(result.ErrorMessage ?? "unknown")}";
|
||||
}
|
||||
|
||||
return View(nameof(TestWebhook));
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> RedeliverWebhook(string webhookId, string deliveryId)
|
||||
{
|
||||
var delivery = await _storeRepo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId);
|
||||
if (delivery is null)
|
||||
return NotFound();
|
||||
|
||||
var newDeliveryId = await _webhookNotificationManager.Redeliver(deliveryId);
|
||||
if (newDeliveryId is null)
|
||||
return NotFound();
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Successfully planned a redelivery";
|
||||
return RedirectToAction(nameof(ModifyWebhook),
|
||||
new
|
||||
{
|
||||
storeId = CurrentStore.Id,
|
||||
webhookId
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> WebhookDelivery(string webhookId, string deliveryId)
|
||||
{
|
||||
var delivery = await _storeRepo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId);
|
||||
if (delivery is null)
|
||||
return NotFound();
|
||||
|
||||
return File(delivery.GetBlob().Request, "application/json");
|
||||
}
|
||||
return File(delivery.GetBlob().Request, "application/json");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,328 +16,327 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
namespace BTCPayServer.Controllers;
|
||||
|
||||
public partial class UIStoresController
|
||||
{
|
||||
public partial class UIStoresController
|
||||
[HttpGet("{storeId}/lightning/{cryptoCode}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult Lightning(string storeId, string cryptoCode)
|
||||
{
|
||||
[HttpGet("{storeId}/lightning/{cryptoCode}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult Lightning(string storeId, string cryptoCode)
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var vm = new LightningViewModel
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
CryptoCode = cryptoCode,
|
||||
StoreId = storeId
|
||||
};
|
||||
SetExistingValues(store, vm);
|
||||
|
||||
var vm = new LightningViewModel
|
||||
{
|
||||
CryptoCode = cryptoCode,
|
||||
StoreId = storeId
|
||||
};
|
||||
SetExistingValues(store, vm);
|
||||
|
||||
if (vm.LightningNodeType == LightningNodeType.Internal)
|
||||
{
|
||||
var services = _externalServiceOptions.Value.ExternalServices.ToList()
|
||||
.Where(service => ExternalServices.LightningServiceTypes.Contains(service.Type))
|
||||
.Select(async service =>
|
||||
{
|
||||
var model = new AdditionalServiceViewModel
|
||||
{
|
||||
DisplayName = service.DisplayName,
|
||||
ServiceName = service.ServiceName,
|
||||
CryptoCode = service.CryptoCode,
|
||||
Type = service.Type.ToString()
|
||||
};
|
||||
try
|
||||
{
|
||||
model.Link = await service.GetLink(Request.GetAbsoluteUriNoPathBase(), _btcpayServerOptions.NetworkType);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
model.Error = exception.Message;
|
||||
}
|
||||
return model;
|
||||
})
|
||||
.Select(t => t.Result)
|
||||
.ToList();
|
||||
|
||||
// other services
|
||||
foreach ((string key, Uri value) in _externalServiceOptions.Value.OtherExternalServices)
|
||||
if (vm.LightningNodeType == LightningNodeType.Internal)
|
||||
{
|
||||
var services = _externalServiceOptions.Value.ExternalServices.ToList()
|
||||
.Where(service => ExternalServices.LightningServiceTypes.Contains(service.Type))
|
||||
.Select(async service =>
|
||||
{
|
||||
if (ExternalServices.LightningServiceNames.Contains(key))
|
||||
var model = new AdditionalServiceViewModel
|
||||
{
|
||||
services.Add(new AdditionalServiceViewModel
|
||||
{
|
||||
DisplayName = key,
|
||||
ServiceName = key,
|
||||
Type = key.Replace(" ", ""),
|
||||
Link = Request.GetAbsoluteUriNoPathBase(value).AbsoluteUri
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
vm.Services = services;
|
||||
}
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/lightning/{cryptoCode}/setup")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult SetupLightningNode(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var vm = new LightningNodeViewModel
|
||||
{
|
||||
CryptoCode = cryptoCode,
|
||||
StoreId = storeId
|
||||
};
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/lightning/{cryptoCode}/setup")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> SetupLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
|
||||
{
|
||||
vm.CryptoCode = cryptoCode;
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var network = _explorerProvider.GetNetwork(vm.CryptoCode);
|
||||
var oldConf = _handlers.GetLightningConfig(store, network);
|
||||
|
||||
vm.CanUseInternalNode = CanUseInternalLightning(vm.CryptoCode);
|
||||
|
||||
if (vm.CryptoCode == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
|
||||
var paymentMethodId = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode);
|
||||
|
||||
LightningPaymentMethodConfig? paymentMethod;
|
||||
if (vm.LightningNodeType == LightningNodeType.Internal)
|
||||
{
|
||||
paymentMethod = new LightningPaymentMethodConfig();
|
||||
paymentMethod.SetInternalNode();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrEmpty(vm.ConnectionString))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "Please provide a connection string");
|
||||
return View(vm);
|
||||
}
|
||||
paymentMethod = new LightningPaymentMethodConfig { ConnectionString = vm.ConnectionString };
|
||||
}
|
||||
|
||||
var handler = (LightningLikePaymentHandler)_handlers[paymentMethodId];
|
||||
var ctx = new PaymentMethodConfigValidationContext(_authorizationService, ModelState,
|
||||
JToken.FromObject(paymentMethod, handler.Serializer), User, oldConf is null ? null : JToken.FromObject(oldConf, handler.Serializer));
|
||||
await handler.ValidatePaymentMethodConfig(ctx);
|
||||
if (ctx.MissingPermission is not null)
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "You do not have the permissions to change this settings");
|
||||
if (!ModelState.IsValid)
|
||||
return View(vm);
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case "save":
|
||||
var lnurl = PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode);
|
||||
store.SetPaymentMethodConfig(_handlers[paymentMethodId], paymentMethod);
|
||||
store.SetPaymentMethodConfig(_handlers[lnurl], new LNURLPaymentMethodConfig
|
||||
{
|
||||
UseBech32Scheme = true,
|
||||
LUD12Enabled = false
|
||||
});
|
||||
|
||||
await _storeRepo.UpdateStore(store);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning node updated.";
|
||||
return RedirectToAction(nameof(LightningSettings), new { storeId, cryptoCode });
|
||||
|
||||
case "test":
|
||||
DisplayName = service.DisplayName,
|
||||
ServiceName = service.ServiceName,
|
||||
CryptoCode = service.CryptoCode,
|
||||
Type = service.Type.ToString()
|
||||
};
|
||||
try
|
||||
{
|
||||
var info = await handler.GetNodeInfo(paymentMethod, null, Request.IsOnion(), true);
|
||||
var hasPublicAddress = info.Any();
|
||||
if (!vm.SkipPortTest && hasPublicAddress)
|
||||
{
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
|
||||
await handler.TestConnection(info.First(), cts.Token);
|
||||
}
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Connection to the Lightning node successful" + (hasPublicAddress
|
||||
? $". Your node address: {info.First()}"
|
||||
: ", but no public address has been configured");
|
||||
model.Link = await service.GetLink(Request.GetAbsoluteUriNoPathBase(), _btcpayServerOptions.NetworkType);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception exception)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = ex.Message;
|
||||
return View(vm);
|
||||
model.Error = exception.Message;
|
||||
}
|
||||
return View(vm);
|
||||
return model;
|
||||
})
|
||||
.Select(t => t.Result)
|
||||
.ToList();
|
||||
|
||||
default:
|
||||
return View(vm);
|
||||
// other services
|
||||
foreach ((string key, Uri value) in _externalServiceOptions.Value.OtherExternalServices)
|
||||
{
|
||||
if (ExternalServices.LightningServiceNames.Contains(key))
|
||||
{
|
||||
services.Add(new AdditionalServiceViewModel
|
||||
{
|
||||
DisplayName = key,
|
||||
ServiceName = key,
|
||||
Type = key.Replace(" ", ""),
|
||||
Link = Request.GetAbsoluteUriNoPathBase(value).AbsoluteUri
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
vm.Services = services;
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/lightning/{cryptoCode}/settings")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult LightningSettings(string storeId, string cryptoCode)
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/lightning/{cryptoCode}/setup")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult SetupLightningNode(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var vm = new LightningNodeViewModel
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
CryptoCode = cryptoCode,
|
||||
StoreId = storeId
|
||||
};
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var excludeFilters = storeBlob.GetExcludedPaymentMethods();
|
||||
var lnId = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
|
||||
var lightning = GetConfig<LightningPaymentMethodConfig>(lnId, store);
|
||||
if (lightning == null)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "You need to connect to a Lightning node before adjusting its settings.";
|
||||
[HttpPost("{storeId}/lightning/{cryptoCode}/setup")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> SetupLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
|
||||
{
|
||||
vm.CryptoCode = cryptoCode;
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
return RedirectToAction(nameof(SetupLightningNode), new { storeId, cryptoCode });
|
||||
}
|
||||
var network = _explorerProvider.GetNetwork(vm.CryptoCode);
|
||||
var oldConf = _handlers.GetLightningConfig(store, network);
|
||||
|
||||
var vm = new LightningSettingsViewModel
|
||||
{
|
||||
CryptoCode = cryptoCode,
|
||||
StoreId = storeId,
|
||||
Enabled = !excludeFilters.Match(lnId),
|
||||
LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate,
|
||||
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
|
||||
LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints,
|
||||
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback
|
||||
};
|
||||
SetExistingValues(store, vm);
|
||||
|
||||
var lnurlId = PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode);
|
||||
var lnurl = GetConfig<LNURLPaymentMethodConfig>(lnurlId, store);
|
||||
if (lnurl != null)
|
||||
{
|
||||
vm.LNURLEnabled = !store.GetStoreBlob().GetExcludedPaymentMethods().Match(lnurlId);
|
||||
vm.LNURLBech32Mode = lnurl.UseBech32Scheme;
|
||||
vm.LUD12Enabled = lnurl.LUD12Enabled;
|
||||
}
|
||||
vm.CanUseInternalNode = CanUseInternalLightning(vm.CryptoCode);
|
||||
|
||||
if (vm.CryptoCode == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/lightning/{cryptoCode}/settings")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> LightningSettings(LightningSettingsViewModel vm)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var paymentMethodId = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode);
|
||||
|
||||
if (vm.CryptoCode == null)
|
||||
LightningPaymentMethodConfig? paymentMethod;
|
||||
if (vm.LightningNodeType == LightningNodeType.Internal)
|
||||
{
|
||||
paymentMethod = new LightningPaymentMethodConfig();
|
||||
paymentMethod.SetInternalNode();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrEmpty(vm.ConnectionString))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network");
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "Please provide a connection string");
|
||||
return View(vm);
|
||||
}
|
||||
paymentMethod = new LightningPaymentMethodConfig { ConnectionString = vm.ConnectionString };
|
||||
}
|
||||
|
||||
var network = _explorerProvider.GetNetwork(vm.CryptoCode);
|
||||
var needUpdate = false;
|
||||
var blob = store.GetStoreBlob();
|
||||
blob.LightningDescriptionTemplate = vm.LightningDescriptionTemplate ?? string.Empty;
|
||||
blob.LightningAmountInSatoshi = vm.LightningAmountInSatoshi;
|
||||
blob.LightningPrivateRouteHints = vm.LightningPrivateRouteHints;
|
||||
blob.OnChainWithLnInvoiceFallback = vm.OnChainWithLnInvoiceFallback;
|
||||
var lnurlId = PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode);
|
||||
blob.SetExcluded(lnurlId, !vm.LNURLEnabled);
|
||||
var handler = (LightningLikePaymentHandler)_handlers[paymentMethodId];
|
||||
var ctx = new PaymentMethodConfigValidationContext(_authorizationService, ModelState,
|
||||
JToken.FromObject(paymentMethod, handler.Serializer), User, oldConf is null ? null : JToken.FromObject(oldConf, handler.Serializer));
|
||||
await handler.ValidatePaymentMethodConfig(ctx);
|
||||
if (ctx.MissingPermission is not null)
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "You do not have the permissions to change this settings");
|
||||
if (!ModelState.IsValid)
|
||||
return View(vm);
|
||||
|
||||
var lnurl = GetConfig<LNURLPaymentMethodConfig>(PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode), store);
|
||||
if (lnurl is null || (
|
||||
lnurl.UseBech32Scheme != vm.LNURLBech32Mode ||
|
||||
lnurl.LUD12Enabled != vm.LUD12Enabled))
|
||||
{
|
||||
needUpdate = true;
|
||||
}
|
||||
switch (command)
|
||||
{
|
||||
case "save":
|
||||
var lnurl = PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode);
|
||||
store.SetPaymentMethodConfig(_handlers[paymentMethodId], paymentMethod);
|
||||
store.SetPaymentMethodConfig(_handlers[lnurl], new LNURLPaymentMethodConfig
|
||||
{
|
||||
UseBech32Scheme = true,
|
||||
LUD12Enabled = false
|
||||
});
|
||||
|
||||
store.SetPaymentMethodConfig(_handlers[lnurlId], new LNURLPaymentMethodConfig
|
||||
{
|
||||
UseBech32Scheme = vm.LNURLBech32Mode,
|
||||
LUD12Enabled = vm.LUD12Enabled
|
||||
});
|
||||
|
||||
if (store.SetStoreBlob(blob))
|
||||
{
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (needUpdate)
|
||||
{
|
||||
await _storeRepo.UpdateStore(store);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning node updated.";
|
||||
return RedirectToAction(nameof(LightningSettings), new { storeId, cryptoCode });
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning settings successfully updated.";
|
||||
}
|
||||
case "test":
|
||||
try
|
||||
{
|
||||
var info = await handler.GetNodeInfo(paymentMethod, null, Request.IsOnion(), true);
|
||||
var hasPublicAddress = info.Any();
|
||||
if (!vm.SkipPortTest && hasPublicAddress)
|
||||
{
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
|
||||
await handler.TestConnection(info.First(), cts.Token);
|
||||
}
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Connection to the Lightning node successful" + (hasPublicAddress
|
||||
? $". Your node address: {info.First()}"
|
||||
: ", but no public address has been configured");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = ex.Message;
|
||||
return View(vm);
|
||||
}
|
||||
return View(vm);
|
||||
|
||||
return RedirectToAction(nameof(LightningSettings), new { vm.StoreId, vm.CryptoCode });
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/lightning/{cryptoCode}/status")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> SetLightningNodeEnabled(string storeId, string cryptoCode, bool enabled)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var network = _explorerProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
|
||||
var lightning = GetConfig<LightningPaymentMethodConfig>(PaymentTypes.LN.GetPaymentMethodId(cryptoCode), store);
|
||||
if (lightning == null)
|
||||
return NotFound();
|
||||
|
||||
var paymentMethodId = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.SetExcluded(paymentMethodId, !enabled);
|
||||
if (!enabled)
|
||||
{
|
||||
storeBlob.SetExcluded(PaymentTypes.LNURL.GetPaymentMethodId(network.CryptoCode), true);
|
||||
}
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _storeRepo.UpdateStore(store);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning payments are now {(enabled ? "enabled" : "disabled")} for this store.";
|
||||
|
||||
return RedirectToAction(nameof(LightningSettings), new { storeId, cryptoCode });
|
||||
}
|
||||
|
||||
private bool CanUseInternalLightning(string cryptoCode)
|
||||
{
|
||||
return _lightningNetworkOptions.InternalLightningByCryptoCode.ContainsKey(cryptoCode.ToUpperInvariant()) && (User.IsInRole(Roles.ServerAdmin) || _policiesSettings.AllowLightningInternalNodeForAll);
|
||||
}
|
||||
|
||||
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
|
||||
{
|
||||
vm.CanUseInternalNode = CanUseInternalLightning(vm.CryptoCode);
|
||||
var lightning = GetConfig<LightningPaymentMethodConfig>(PaymentTypes.LN.GetPaymentMethodId(vm.CryptoCode), store);
|
||||
|
||||
if (lightning != null)
|
||||
{
|
||||
vm.LightningNodeType = lightning.IsInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
|
||||
vm.ConnectionString = lightning.GetDisplayableConnectionString();
|
||||
}
|
||||
else
|
||||
{
|
||||
vm.LightningNodeType = vm.CanUseInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
|
||||
}
|
||||
}
|
||||
|
||||
private T? GetConfig<T>(PaymentMethodId paymentMethodId, StoreData store) where T: class
|
||||
{
|
||||
return store.GetPaymentMethodConfig<T>(paymentMethodId, _handlers);
|
||||
default:
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/lightning/{cryptoCode}/settings")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult LightningSettings(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var excludeFilters = storeBlob.GetExcludedPaymentMethods();
|
||||
var lnId = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
|
||||
var lightning = GetConfig<LightningPaymentMethodConfig>(lnId, store);
|
||||
if (lightning == null)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "You need to connect to a Lightning node before adjusting its settings.";
|
||||
|
||||
return RedirectToAction(nameof(SetupLightningNode), new { storeId, cryptoCode });
|
||||
}
|
||||
|
||||
var vm = new LightningSettingsViewModel
|
||||
{
|
||||
CryptoCode = cryptoCode,
|
||||
StoreId = storeId,
|
||||
Enabled = !excludeFilters.Match(lnId),
|
||||
LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate,
|
||||
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
|
||||
LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints,
|
||||
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback
|
||||
};
|
||||
SetExistingValues(store, vm);
|
||||
|
||||
var lnurlId = PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode);
|
||||
var lnurl = GetConfig<LNURLPaymentMethodConfig>(lnurlId, store);
|
||||
if (lnurl != null)
|
||||
{
|
||||
vm.LNURLEnabled = !store.GetStoreBlob().GetExcludedPaymentMethods().Match(lnurlId);
|
||||
vm.LNURLBech32Mode = lnurl.UseBech32Scheme;
|
||||
vm.LUD12Enabled = lnurl.LUD12Enabled;
|
||||
}
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/lightning/{cryptoCode}/settings")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> LightningSettings(LightningSettingsViewModel vm)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
if (vm.CryptoCode == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var network = _explorerProvider.GetNetwork(vm.CryptoCode);
|
||||
var needUpdate = false;
|
||||
var blob = store.GetStoreBlob();
|
||||
blob.LightningDescriptionTemplate = vm.LightningDescriptionTemplate ?? string.Empty;
|
||||
blob.LightningAmountInSatoshi = vm.LightningAmountInSatoshi;
|
||||
blob.LightningPrivateRouteHints = vm.LightningPrivateRouteHints;
|
||||
blob.OnChainWithLnInvoiceFallback = vm.OnChainWithLnInvoiceFallback;
|
||||
var lnurlId = PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode);
|
||||
blob.SetExcluded(lnurlId, !vm.LNURLEnabled);
|
||||
|
||||
var lnurl = GetConfig<LNURLPaymentMethodConfig>(PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode), store);
|
||||
if (lnurl is null || (
|
||||
lnurl.UseBech32Scheme != vm.LNURLBech32Mode ||
|
||||
lnurl.LUD12Enabled != vm.LUD12Enabled))
|
||||
{
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
store.SetPaymentMethodConfig(_handlers[lnurlId], new LNURLPaymentMethodConfig
|
||||
{
|
||||
UseBech32Scheme = vm.LNURLBech32Mode,
|
||||
LUD12Enabled = vm.LUD12Enabled
|
||||
});
|
||||
|
||||
if (store.SetStoreBlob(blob))
|
||||
{
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (needUpdate)
|
||||
{
|
||||
await _storeRepo.UpdateStore(store);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning settings successfully updated.";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(LightningSettings), new { vm.StoreId, vm.CryptoCode });
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/lightning/{cryptoCode}/status")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> SetLightningNodeEnabled(string storeId, string cryptoCode, bool enabled)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var network = _explorerProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
|
||||
var lightning = GetConfig<LightningPaymentMethodConfig>(PaymentTypes.LN.GetPaymentMethodId(cryptoCode), store);
|
||||
if (lightning == null)
|
||||
return NotFound();
|
||||
|
||||
var paymentMethodId = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.SetExcluded(paymentMethodId, !enabled);
|
||||
if (!enabled)
|
||||
{
|
||||
storeBlob.SetExcluded(PaymentTypes.LNURL.GetPaymentMethodId(network.CryptoCode), true);
|
||||
}
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _storeRepo.UpdateStore(store);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning payments are now {(enabled ? "enabled" : "disabled")} for this store.";
|
||||
|
||||
return RedirectToAction(nameof(LightningSettings), new { storeId, cryptoCode });
|
||||
}
|
||||
|
||||
private bool CanUseInternalLightning(string cryptoCode)
|
||||
{
|
||||
return _lightningNetworkOptions.InternalLightningByCryptoCode.ContainsKey(cryptoCode.ToUpperInvariant()) && (User.IsInRole(Roles.ServerAdmin) || _policiesSettings.AllowLightningInternalNodeForAll);
|
||||
}
|
||||
|
||||
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
|
||||
{
|
||||
vm.CanUseInternalNode = CanUseInternalLightning(vm.CryptoCode);
|
||||
var lightning = GetConfig<LightningPaymentMethodConfig>(PaymentTypes.LN.GetPaymentMethodId(vm.CryptoCode), store);
|
||||
|
||||
if (lightning != null)
|
||||
{
|
||||
vm.LightningNodeType = lightning.IsInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
|
||||
vm.ConnectionString = lightning.GetDisplayableConnectionString();
|
||||
}
|
||||
else
|
||||
{
|
||||
vm.LightningNodeType = vm.CanUseInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
|
||||
}
|
||||
}
|
||||
|
||||
private T? GetConfig<T>(PaymentMethodId paymentMethodId, StoreData store) where T: class
|
||||
{
|
||||
return store.GetPaymentMethodConfig<T>(paymentMethodId, _handlers);
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -14,177 +14,176 @@ using BTCPayServer.Rating;
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
namespace BTCPayServer.Controllers;
|
||||
|
||||
public partial class UIStoresController
|
||||
{
|
||||
public partial class UIStoresController
|
||||
[HttpGet("{storeId}/rates")]
|
||||
public IActionResult Rates()
|
||||
{
|
||||
[HttpGet("{storeId}/rates")]
|
||||
public IActionResult Rates()
|
||||
var exchanges = GetSupportedExchanges().ToList();
|
||||
var storeBlob = CurrentStore.GetStoreBlob();
|
||||
var vm = new RatesViewModel();
|
||||
vm.SetExchangeRates(exchanges, storeBlob.PreferredExchange ?? storeBlob.GetRecommendedExchange());
|
||||
vm.Spread = (double)(storeBlob.Spread * 100m);
|
||||
vm.StoreId = CurrentStore.Id;
|
||||
vm.Script = storeBlob.GetRateRules(_networkProvider).ToString();
|
||||
vm.DefaultScript = storeBlob.GetDefaultRateRules(_networkProvider).ToString();
|
||||
vm.AvailableExchanges = exchanges;
|
||||
vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString();
|
||||
vm.ShowScripting = storeBlob.RateScripting;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/rates")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Rates(RatesViewModel model, string? command = null, string? storeId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (command == "scripting-on")
|
||||
{
|
||||
var exchanges = GetSupportedExchanges().ToList();
|
||||
var storeBlob = CurrentStore.GetStoreBlob();
|
||||
var vm = new RatesViewModel();
|
||||
vm.SetExchangeRates(exchanges, storeBlob.PreferredExchange ?? storeBlob.GetRecommendedExchange());
|
||||
vm.Spread = (double)(storeBlob.Spread * 100m);
|
||||
vm.StoreId = CurrentStore.Id;
|
||||
vm.Script = storeBlob.GetRateRules(_networkProvider).ToString();
|
||||
vm.DefaultScript = storeBlob.GetDefaultRateRules(_networkProvider).ToString();
|
||||
vm.AvailableExchanges = exchanges;
|
||||
vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString();
|
||||
vm.ShowScripting = storeBlob.RateScripting;
|
||||
return View(vm);
|
||||
return RedirectToAction(nameof(ShowRateRules), new { scripting = true, storeId = model.StoreId });
|
||||
}
|
||||
if (command == "scripting-off")
|
||||
{
|
||||
return RedirectToAction(nameof(ShowRateRules), new { scripting = false, storeId = model.StoreId });
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/rates")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Rates(RatesViewModel model, string? command = null, string? storeId = null, CancellationToken cancellationToken = default)
|
||||
var exchanges = GetSupportedExchanges().ToList();
|
||||
model.SetExchangeRates(exchanges, model.PreferredExchange ?? HttpContext.GetStoreData().GetStoreBlob().GetRecommendedExchange());
|
||||
model.StoreId = storeId ?? model.StoreId;
|
||||
CurrencyPair[]? currencyPairs = null;
|
||||
try
|
||||
{
|
||||
if (command == "scripting-on")
|
||||
{
|
||||
return RedirectToAction(nameof(ShowRateRules), new { scripting = true, storeId = model.StoreId });
|
||||
}
|
||||
if (command == "scripting-off")
|
||||
{
|
||||
return RedirectToAction(nameof(ShowRateRules), new { scripting = false, storeId = model.StoreId });
|
||||
}
|
||||
currencyPairs = model.DefaultCurrencyPairs?
|
||||
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(p => CurrencyPair.Parse(p))
|
||||
.ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.DefaultCurrencyPairs), "Invalid currency pairs (should be for example: BTC_USD,BTC_CAD,BTC_JPY)");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
if (model.PreferredExchange != null)
|
||||
model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant();
|
||||
|
||||
var exchanges = GetSupportedExchanges().ToList();
|
||||
model.SetExchangeRates(exchanges, model.PreferredExchange ?? HttpContext.GetStoreData().GetStoreBlob().GetRecommendedExchange());
|
||||
model.StoreId = storeId ?? model.StoreId;
|
||||
CurrencyPair[]? currencyPairs = null;
|
||||
try
|
||||
{
|
||||
currencyPairs = model.DefaultCurrencyPairs?
|
||||
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(p => CurrencyPair.Parse(p))
|
||||
.ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.DefaultCurrencyPairs), "Invalid currency pairs (should be for example: BTC_USD,BTC_CAD,BTC_JPY)");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
var blob = CurrentStore.GetStoreBlob();
|
||||
model.DefaultScript = blob.GetDefaultRateRules(_networkProvider).ToString();
|
||||
model.AvailableExchanges = exchanges;
|
||||
|
||||
blob.PreferredExchange = model.PreferredExchange;
|
||||
blob.Spread = (decimal)model.Spread / 100.0m;
|
||||
blob.DefaultCurrencyPairs = currencyPairs;
|
||||
if (!model.ShowScripting)
|
||||
{
|
||||
if (!exchanges.Any(provider => provider.Id.Equals(model.PreferredExchange, StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
|
||||
return View(model);
|
||||
}
|
||||
if (model.PreferredExchange != null)
|
||||
model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant();
|
||||
|
||||
var blob = CurrentStore.GetStoreBlob();
|
||||
model.DefaultScript = blob.GetDefaultRateRules(_networkProvider).ToString();
|
||||
model.AvailableExchanges = exchanges;
|
||||
|
||||
blob.PreferredExchange = model.PreferredExchange;
|
||||
blob.Spread = (decimal)model.Spread / 100.0m;
|
||||
blob.DefaultCurrencyPairs = currencyPairs;
|
||||
if (!model.ShowScripting)
|
||||
}
|
||||
RateRules? rules;
|
||||
if (model.ShowScripting)
|
||||
{
|
||||
if (!RateRules.TryParse(model.Script, out rules, out var errors))
|
||||
{
|
||||
if (!exchanges.Any(provider => provider.Id.Equals(model.PreferredExchange, StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
RateRules? rules;
|
||||
if (model.ShowScripting)
|
||||
{
|
||||
if (!RateRules.TryParse(model.Script, out rules, out var errors))
|
||||
{
|
||||
errors ??= [];
|
||||
var errorString = string.Join(", ", errors.ToArray());
|
||||
ModelState.AddModelError(nameof(model.Script), $"Parsing error ({errorString})");
|
||||
return View(model);
|
||||
}
|
||||
else
|
||||
{
|
||||
blob.RateScript = rules.ToString();
|
||||
ModelState.Remove(nameof(model.Script));
|
||||
model.Script = blob.RateScript;
|
||||
}
|
||||
}
|
||||
rules = blob.GetRateRules(_networkProvider);
|
||||
|
||||
if (command == "Test")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(model.ScriptTest))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.ScriptTest), "Fill out currency pair to test for (like BTC_USD,BTC_CAD)");
|
||||
return View(model);
|
||||
}
|
||||
var splitted = model.ScriptTest.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var pairs = new List<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);
|
||||
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)
|
||||
: testResult.EvaluatedRule
|
||||
});
|
||||
}
|
||||
model.TestRateRules = testResults;
|
||||
errors ??= [];
|
||||
var errorString = string.Join(", ", errors.ToArray());
|
||||
ModelState.AddModelError(nameof(model.Script), $"Parsing error ({errorString})");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// command == Save
|
||||
if (CurrentStore.SetStoreBlob(blob))
|
||||
else
|
||||
{
|
||||
await _storeRepo.UpdateStore(CurrentStore);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Rate settings updated";
|
||||
blob.RateScript = rules.ToString();
|
||||
ModelState.Remove(nameof(model.Script));
|
||||
model.Script = blob.RateScript;
|
||||
}
|
||||
return RedirectToAction(nameof(Rates), new
|
||||
}
|
||||
rules = blob.GetRateRules(_networkProvider);
|
||||
|
||||
if (command == "Test")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(model.ScriptTest))
|
||||
{
|
||||
storeId = CurrentStore.Id
|
||||
});
|
||||
ModelState.AddModelError(nameof(model.ScriptTest), "Fill out currency pair to test for (like BTC_USD,BTC_CAD)");
|
||||
return View(model);
|
||||
}
|
||||
var splitted = model.ScriptTest.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var pairs = new List<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);
|
||||
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)
|
||||
: testResult.EvaluatedRule
|
||||
});
|
||||
}
|
||||
model.TestRateRules = testResults;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/rates/confirm")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult ShowRateRules(bool scripting)
|
||||
// command == Save
|
||||
if (CurrentStore.SetStoreBlob(blob))
|
||||
{
|
||||
return View("Confirm", new ConfirmModel
|
||||
{
|
||||
Action = "Continue",
|
||||
Title = "Rate rule scripting",
|
||||
Description = scripting ?
|
||||
"This action will modify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)"
|
||||
: "This action will delete your rate script. Are you sure to turn off rate rules scripting?",
|
||||
ButtonClass = scripting ? "btn-primary" : "btn-danger"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/rates/confirm")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ShowRateRulesPost(bool scripting)
|
||||
{
|
||||
var blob = CurrentStore.GetStoreBlob();
|
||||
blob.RateScripting = scripting;
|
||||
blob.RateScript = blob.GetDefaultRateRules(_networkProvider).ToString();
|
||||
CurrentStore.SetStoreBlob(blob);
|
||||
await _storeRepo.UpdateStore(CurrentStore);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Rate rules scripting " + (scripting ? "activated" : "deactivated");
|
||||
return RedirectToAction(nameof(Rates), new { storeId = CurrentStore.Id });
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Rate settings updated";
|
||||
}
|
||||
|
||||
private IEnumerable<RateSourceInfo> GetSupportedExchanges()
|
||||
return RedirectToAction(nameof(Rates), new
|
||||
{
|
||||
return _rateFactory.RateProviderFactory.AvailableRateProviders
|
||||
.OrderBy(s => s.DisplayName, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
storeId = CurrentStore.Id
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/rates/confirm")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult ShowRateRules(bool scripting)
|
||||
{
|
||||
return View("Confirm", new ConfirmModel
|
||||
{
|
||||
Action = "Continue",
|
||||
Title = "Rate rule scripting",
|
||||
Description = scripting ?
|
||||
"This action will modify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)"
|
||||
: "This action will delete your rate script. Are you sure to turn off rate rules scripting?",
|
||||
ButtonClass = scripting ? "btn-primary" : "btn-danger"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/rates/confirm")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ShowRateRulesPost(bool scripting)
|
||||
{
|
||||
var blob = CurrentStore.GetStoreBlob();
|
||||
blob.RateScripting = scripting;
|
||||
blob.RateScript = blob.GetDefaultRateRules(_networkProvider).ToString();
|
||||
CurrentStore.SetStoreBlob(blob);
|
||||
await _storeRepo.UpdateStore(CurrentStore);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Rate rules scripting " + (scripting ? "activated" : "deactivated");
|
||||
return RedirectToAction(nameof(Rates), new { storeId = CurrentStore.Id });
|
||||
}
|
||||
|
||||
private IEnumerable<RateSourceInfo> GetSupportedExchanges()
|
||||
{
|
||||
return _rateFactory.RateProviderFactory.AvailableRateProviders
|
||||
.OrderBy(s => s.DisplayName, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,151 +9,150 @@ using BTCPayServer.Services.Stores;
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
namespace BTCPayServer.Controllers;
|
||||
|
||||
public partial class UIStoresController
|
||||
{
|
||||
public partial class UIStoresController
|
||||
[HttpGet("{storeId}/roles")]
|
||||
public async Task<IActionResult> ListRoles(
|
||||
string storeId,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
RolesViewModel model,
|
||||
string sortOrder = null
|
||||
)
|
||||
{
|
||||
[HttpGet("{storeId}/roles")]
|
||||
public async Task<IActionResult> ListRoles(
|
||||
string storeId,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
RolesViewModel model,
|
||||
string sortOrder = null
|
||||
)
|
||||
var roles = await storeRepository.GetStoreRoles(storeId, true);
|
||||
var defaultRole = (await storeRepository.GetDefaultRole()).Role;
|
||||
model ??= new RolesViewModel();
|
||||
model.DefaultRole = defaultRole;
|
||||
|
||||
switch (sortOrder)
|
||||
{
|
||||
var roles = await storeRepository.GetStoreRoles(storeId, true);
|
||||
var defaultRole = (await storeRepository.GetDefaultRole()).Role;
|
||||
model ??= new RolesViewModel();
|
||||
model.DefaultRole = defaultRole;
|
||||
|
||||
switch (sortOrder)
|
||||
{
|
||||
case "desc":
|
||||
ViewData["NextRoleSortOrder"] = "asc";
|
||||
roles = roles.OrderByDescending(user => user.Role).ToArray();
|
||||
break;
|
||||
case "asc":
|
||||
roles = roles.OrderBy(user => user.Role).ToArray();
|
||||
ViewData["NextRoleSortOrder"] = "desc";
|
||||
break;
|
||||
}
|
||||
|
||||
model.Roles = roles.Skip(model.Skip).Take(model.Count).ToList();
|
||||
|
||||
return View(model);
|
||||
case "desc":
|
||||
ViewData["NextRoleSortOrder"] = "asc";
|
||||
roles = roles.OrderByDescending(user => user.Role).ToArray();
|
||||
break;
|
||||
case "asc":
|
||||
roles = roles.OrderBy(user => user.Role).ToArray();
|
||||
ViewData["NextRoleSortOrder"] = "desc";
|
||||
break;
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/roles/{role}")]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> CreateOrEditRole(
|
||||
string storeId,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
{
|
||||
if (role == "create")
|
||||
{
|
||||
ModelState.Remove(nameof(role));
|
||||
return View(new UpdateRoleViewModel());
|
||||
}
|
||||
model.Roles = roles.Skip(model.Skip).Take(model.Count).ToList();
|
||||
|
||||
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(storeId, role));
|
||||
if (roleData == null)
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/roles/{role}")]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> CreateOrEditRole(
|
||||
string storeId,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
{
|
||||
if (role == "create")
|
||||
{
|
||||
ModelState.Remove(nameof(role));
|
||||
return View(new UpdateRoleViewModel());
|
||||
}
|
||||
|
||||
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(storeId, role));
|
||||
if (roleData == null)
|
||||
return NotFound();
|
||||
return View(new UpdateRoleViewModel
|
||||
{
|
||||
Policies = roleData.Permissions,
|
||||
Role = roleData.Role
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/roles/{role}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> CreateOrEditRole(
|
||||
string storeId,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
[FromRoute] string role, UpdateRoleViewModel viewModel)
|
||||
{
|
||||
string successMessage;
|
||||
StoreRoleId roleId;
|
||||
if (role == "create")
|
||||
{
|
||||
successMessage = "Role created";
|
||||
role = viewModel.Role;
|
||||
roleId = new StoreRoleId(storeId, role);
|
||||
}
|
||||
else
|
||||
{
|
||||
successMessage = "Role updated";
|
||||
roleId = new StoreRoleId(storeId, role);
|
||||
var storeRole = await storeRepository.GetStoreRole(roleId);
|
||||
if (storeRole == null)
|
||||
return NotFound();
|
||||
return View(new UpdateRoleViewModel
|
||||
{
|
||||
Policies = roleData.Permissions,
|
||||
Role = roleData.Role
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/roles/{role}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> CreateOrEditRole(
|
||||
string storeId,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
[FromRoute] string role, UpdateRoleViewModel viewModel)
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
string successMessage;
|
||||
StoreRoleId roleId;
|
||||
if (role == "create")
|
||||
{
|
||||
successMessage = "Role created";
|
||||
role = viewModel.Role;
|
||||
roleId = new StoreRoleId(storeId, role);
|
||||
}
|
||||
else
|
||||
{
|
||||
successMessage = "Role updated";
|
||||
roleId = new StoreRoleId(storeId, role);
|
||||
var storeRole = await storeRepository.GetStoreRole(roleId);
|
||||
if (storeRole == null)
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
var r = await storeRepository.AddOrUpdateStoreRole(roleId, viewModel.Policies);
|
||||
if (r is null)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Role could not be updated"
|
||||
});
|
||||
return View(viewModel);
|
||||
}
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
var r = await storeRepository.AddOrUpdateStoreRole(roleId, viewModel.Policies);
|
||||
if (r is null)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = successMessage
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Role could not be updated"
|
||||
});
|
||||
|
||||
return RedirectToAction(nameof(ListRoles), new { storeId });
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/roles/{role}/delete")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> DeleteRole(
|
||||
string storeId,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(storeId, role), true);
|
||||
if (roleData == null)
|
||||
return NotFound();
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = successMessage
|
||||
});
|
||||
|
||||
return View("Confirm",
|
||||
roleData.IsUsed is true
|
||||
? new ConfirmModel("Delete role",
|
||||
$"Unable to proceed: The role <strong>{_html.Encode(roleData.Role)}</strong> is currently assigned to one or more users, it cannot be removed.")
|
||||
: new ConfirmModel("Delete role",
|
||||
$"The role <strong>{_html.Encode(roleData.Role)}</strong> will be permanently deleted. Are you sure?",
|
||||
"Delete"));
|
||||
}
|
||||
return RedirectToAction(nameof(ListRoles), new { storeId });
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/roles/{role}/delete")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> DeleteRolePost(
|
||||
string storeId,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
[HttpGet("{storeId}/roles/{role}/delete")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> DeleteRole(
|
||||
string storeId,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
{
|
||||
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(storeId, role), true);
|
||||
if (roleData == null)
|
||||
return NotFound();
|
||||
|
||||
return View("Confirm",
|
||||
roleData.IsUsed is true
|
||||
? new ConfirmModel("Delete role",
|
||||
$"Unable to proceed: The role <strong>{_html.Encode(roleData.Role)}</strong> is currently assigned to one or more users, it cannot be removed.")
|
||||
: new ConfirmModel("Delete role",
|
||||
$"The role <strong>{_html.Encode(roleData.Role)}</strong> will be permanently deleted. Are you sure?",
|
||||
"Delete"));
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/roles/{role}/delete")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> DeleteRolePost(
|
||||
string storeId,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
{
|
||||
var roleId = new StoreRoleId(storeId, role);
|
||||
var roleData = await storeRepository.GetStoreRole(roleId, true);
|
||||
if (roleData == null)
|
||||
return NotFound();
|
||||
if (roleData.IsUsed is true)
|
||||
{
|
||||
var roleId = new StoreRoleId(storeId, role);
|
||||
var roleData = await storeRepository.GetStoreRole(roleId, true);
|
||||
if (roleData == null)
|
||||
return NotFound();
|
||||
if (roleData.IsUsed is true)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
await storeRepository.RemoveStoreRole(roleId);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Role deleted";
|
||||
return RedirectToAction(nameof(ListRoles), new { storeId });
|
||||
return BadRequest();
|
||||
}
|
||||
await storeRepository.RemoveStoreRole(roleId);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Role deleted";
|
||||
return RedirectToAction(nameof(ListRoles), new { storeId });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,226 +14,226 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
namespace BTCPayServer.Controllers;
|
||||
|
||||
public partial class UIStoresController
|
||||
{
|
||||
public partial class UIStoresController
|
||||
[HttpGet("{storeId}/settings")]
|
||||
public IActionResult GeneralSettings()
|
||||
{
|
||||
[HttpGet("{storeId}/settings")]
|
||||
public IActionResult GeneralSettings()
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var vm = new GeneralSettingsViewModel
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
Id = store.Id,
|
||||
StoreName = store.StoreName,
|
||||
StoreWebsite = store.StoreWebsite,
|
||||
LogoFileId = storeBlob.LogoFileId,
|
||||
CssFileId = storeBlob.CssFileId,
|
||||
BrandColor = storeBlob.BrandColor,
|
||||
NetworkFeeMode = storeBlob.NetworkFeeMode,
|
||||
AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice,
|
||||
PaymentTolerance = storeBlob.PaymentTolerance,
|
||||
InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes,
|
||||
DefaultCurrency = storeBlob.DefaultCurrency,
|
||||
BOLT11Expiration = (long)storeBlob.RefundBOLT11Expiration.TotalDays,
|
||||
Archived = store.Archived,
|
||||
CanDelete = _storeRepo.CanDeleteStores()
|
||||
};
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var vm = new GeneralSettingsViewModel
|
||||
{
|
||||
Id = store.Id,
|
||||
StoreName = store.StoreName,
|
||||
StoreWebsite = store.StoreWebsite,
|
||||
LogoFileId = storeBlob.LogoFileId,
|
||||
CssFileId = storeBlob.CssFileId,
|
||||
BrandColor = storeBlob.BrandColor,
|
||||
NetworkFeeMode = storeBlob.NetworkFeeMode,
|
||||
AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice,
|
||||
PaymentTolerance = storeBlob.PaymentTolerance,
|
||||
InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes,
|
||||
DefaultCurrency = storeBlob.DefaultCurrency,
|
||||
BOLT11Expiration = (long)storeBlob.RefundBOLT11Expiration.TotalDays,
|
||||
Archived = store.Archived,
|
||||
CanDelete = _storeRepo.CanDeleteStores()
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
return View(vm);
|
||||
[HttpPost("{storeId}/settings")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> GeneralSettings(
|
||||
GeneralSettingsViewModel model,
|
||||
[FromForm] bool RemoveLogoFile = false,
|
||||
[FromForm] bool RemoveCssFile = false)
|
||||
{
|
||||
bool needUpdate = false;
|
||||
if (CurrentStore.StoreName != model.StoreName)
|
||||
{
|
||||
needUpdate = true;
|
||||
CurrentStore.StoreName = model.StoreName;
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/settings")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> GeneralSettings(
|
||||
GeneralSettingsViewModel model,
|
||||
[FromForm] bool RemoveLogoFile = false,
|
||||
[FromForm] bool RemoveCssFile = false)
|
||||
if (CurrentStore.StoreWebsite != model.StoreWebsite)
|
||||
{
|
||||
bool needUpdate = false;
|
||||
if (CurrentStore.StoreName != model.StoreName)
|
||||
needUpdate = true;
|
||||
CurrentStore.StoreWebsite = model.StoreWebsite;
|
||||
}
|
||||
|
||||
var blob = CurrentStore.GetStoreBlob();
|
||||
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
|
||||
blob.NetworkFeeMode = model.NetworkFeeMode;
|
||||
blob.PaymentTolerance = model.PaymentTolerance;
|
||||
blob.DefaultCurrency = model.DefaultCurrency;
|
||||
blob.InvoiceExpiration = TimeSpan.FromMinutes(model.InvoiceExpiration);
|
||||
blob.RefundBOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration);
|
||||
if (!string.IsNullOrEmpty(model.BrandColor) && !ColorPalette.IsValid(model.BrandColor))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.BrandColor), "Invalid color");
|
||||
return View(model);
|
||||
}
|
||||
blob.BrandColor = model.BrandColor;
|
||||
|
||||
var userId = GetUserId();
|
||||
if (userId is null)
|
||||
return NotFound();
|
||||
|
||||
if (model.LogoFile != null)
|
||||
{
|
||||
if (model.LogoFile.Length > 1_000_000)
|
||||
{
|
||||
needUpdate = true;
|
||||
CurrentStore.StoreName = model.StoreName;
|
||||
ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file should be less than 1MB");
|
||||
}
|
||||
|
||||
if (CurrentStore.StoreWebsite != model.StoreWebsite)
|
||||
else if (!model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
|
||||
{
|
||||
needUpdate = true;
|
||||
CurrentStore.StoreWebsite = model.StoreWebsite;
|
||||
ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file needs to be an image");
|
||||
}
|
||||
|
||||
var blob = CurrentStore.GetStoreBlob();
|
||||
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
|
||||
blob.NetworkFeeMode = model.NetworkFeeMode;
|
||||
blob.PaymentTolerance = model.PaymentTolerance;
|
||||
blob.DefaultCurrency = model.DefaultCurrency;
|
||||
blob.InvoiceExpiration = TimeSpan.FromMinutes(model.InvoiceExpiration);
|
||||
blob.RefundBOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration);
|
||||
if (!string.IsNullOrEmpty(model.BrandColor) && !ColorPalette.IsValid(model.BrandColor))
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.BrandColor), "Invalid color");
|
||||
return View(model);
|
||||
}
|
||||
blob.BrandColor = model.BrandColor;
|
||||
|
||||
var userId = GetUserId();
|
||||
if (userId is null)
|
||||
return NotFound();
|
||||
|
||||
if (model.LogoFile != null)
|
||||
{
|
||||
if (model.LogoFile.Length > 1_000_000)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file should be less than 1MB");
|
||||
}
|
||||
else if (!model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
|
||||
var formFile = await model.LogoFile.Bufferize();
|
||||
if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file needs to be an image");
|
||||
}
|
||||
else
|
||||
{
|
||||
var formFile = await model.LogoFile.Bufferize();
|
||||
if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file needs to be an image");
|
||||
}
|
||||
else
|
||||
{
|
||||
model.LogoFile = formFile;
|
||||
// delete existing file
|
||||
if (!string.IsNullOrEmpty(blob.LogoFileId))
|
||||
{
|
||||
await _fileService.RemoveFile(blob.LogoFileId, userId);
|
||||
}
|
||||
// add new image
|
||||
try
|
||||
{
|
||||
var storedFile = await _fileService.AddFile(model.LogoFile, userId);
|
||||
blob.LogoFileId = storedFile.Id;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.LogoFile), $"Could not save logo: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (RemoveLogoFile && !string.IsNullOrEmpty(blob.LogoFileId))
|
||||
{
|
||||
await _fileService.RemoveFile(blob.LogoFileId, userId);
|
||||
blob.LogoFileId = null;
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (model.CssFile != null)
|
||||
{
|
||||
if (model.CssFile.Length > 1_000_000)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.CssFile), "The uploaded file should be less than 1MB");
|
||||
}
|
||||
else if (!model.CssFile.ContentType.Equals("text/css", StringComparison.InvariantCulture))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.CssFile), "The uploaded file needs to be a CSS file");
|
||||
}
|
||||
else if (!model.CssFile.FileName.EndsWith(".css", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.CssFile), "The uploaded file needs to be a CSS file");
|
||||
}
|
||||
else
|
||||
{
|
||||
model.LogoFile = formFile;
|
||||
// delete existing file
|
||||
if (!string.IsNullOrEmpty(blob.CssFileId))
|
||||
if (!string.IsNullOrEmpty(blob.LogoFileId))
|
||||
{
|
||||
await _fileService.RemoveFile(blob.CssFileId, userId);
|
||||
await _fileService.RemoveFile(blob.LogoFileId, userId);
|
||||
}
|
||||
// add new file
|
||||
// add new image
|
||||
try
|
||||
{
|
||||
var storedFile = await _fileService.AddFile(model.CssFile, userId);
|
||||
blob.CssFileId = storedFile.Id;
|
||||
var storedFile = await _fileService.AddFile(model.LogoFile, userId);
|
||||
blob.LogoFileId = storedFile.Id;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.CssFile), $"Could not save CSS file: {e.Message}");
|
||||
ModelState.AddModelError(nameof(model.LogoFile), $"Could not save logo: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (RemoveCssFile && !string.IsNullOrEmpty(blob.CssFileId))
|
||||
{
|
||||
await _fileService.RemoveFile(blob.CssFileId, userId);
|
||||
blob.CssFileId = null;
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (CurrentStore.SetStoreBlob(blob))
|
||||
{
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (needUpdate)
|
||||
{
|
||||
await _storeRepo.UpdateStore(CurrentStore);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(GeneralSettings), new
|
||||
{
|
||||
storeId = CurrentStore.Id
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/archive")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ToggleArchive(string storeId)
|
||||
else if (RemoveLogoFile && !string.IsNullOrEmpty(blob.LogoFileId))
|
||||
{
|
||||
await _fileService.RemoveFile(blob.LogoFileId, userId);
|
||||
blob.LogoFileId = null;
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (model.CssFile != null)
|
||||
{
|
||||
if (model.CssFile.Length > 1_000_000)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.CssFile), "The uploaded file should be less than 1MB");
|
||||
}
|
||||
else if (!model.CssFile.ContentType.Equals("text/css", StringComparison.InvariantCulture))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.CssFile), "The uploaded file needs to be a CSS file");
|
||||
}
|
||||
else if (!model.CssFile.FileName.EndsWith(".css", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.CssFile), "The uploaded file needs to be a CSS file");
|
||||
}
|
||||
else
|
||||
{
|
||||
// delete existing file
|
||||
if (!string.IsNullOrEmpty(blob.CssFileId))
|
||||
{
|
||||
await _fileService.RemoveFile(blob.CssFileId, userId);
|
||||
}
|
||||
// add new file
|
||||
try
|
||||
{
|
||||
var storedFile = await _fileService.AddFile(model.CssFile, userId);
|
||||
blob.CssFileId = storedFile.Id;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.CssFile), $"Could not save CSS file: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (RemoveCssFile && !string.IsNullOrEmpty(blob.CssFileId))
|
||||
{
|
||||
await _fileService.RemoveFile(blob.CssFileId, userId);
|
||||
blob.CssFileId = null;
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (CurrentStore.SetStoreBlob(blob))
|
||||
{
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (needUpdate)
|
||||
{
|
||||
CurrentStore.Archived = !CurrentStore.Archived;
|
||||
await _storeRepo.UpdateStore(CurrentStore);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = CurrentStore.Archived
|
||||
? "The store has been archived and will no longer appear in the stores list by default."
|
||||
: "The store has been unarchived and will appear in the stores list by default again.";
|
||||
|
||||
return RedirectToAction(nameof(GeneralSettings), new
|
||||
{
|
||||
storeId = CurrentStore.Id
|
||||
});
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated";
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/delete")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult DeleteStore(string storeId)
|
||||
return RedirectToAction(nameof(GeneralSettings), new
|
||||
{
|
||||
return View("Confirm", new ConfirmModel("Delete store", "The store will be permanently deleted. This action will also delete all invoices, apps and data associated with the store. Are you sure?", "Delete"));
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/delete")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> DeleteStorePost(string storeId)
|
||||
{
|
||||
await _storeRepo.DeleteStore(CurrentStore.Id);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Store successfully deleted.";
|
||||
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
|
||||
}
|
||||
storeId = CurrentStore.Id
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/checkout")]
|
||||
public IActionResult CheckoutAppearance()
|
||||
[HttpPost("{storeId}/archive")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ToggleArchive(string storeId)
|
||||
{
|
||||
CurrentStore.Archived = !CurrentStore.Archived;
|
||||
await _storeRepo.UpdateStore(CurrentStore);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = CurrentStore.Archived
|
||||
? "The store has been archived and will no longer appear in the stores list by default."
|
||||
: "The store has been unarchived and will appear in the stores list by default again.";
|
||||
|
||||
return RedirectToAction(nameof(GeneralSettings), new
|
||||
{
|
||||
var storeBlob = CurrentStore.GetStoreBlob();
|
||||
var vm = new CheckoutAppearanceViewModel();
|
||||
SetCryptoCurrencies(vm, CurrentStore);
|
||||
vm.PaymentMethodCriteria = CurrentStore.GetPaymentMethodConfigs(_handlers)
|
||||
.Where(s => !storeBlob.GetExcludedPaymentMethods().Match(s.Key) && s.Value is not LNURLPaymentMethodConfig)
|
||||
.Select(c =>
|
||||
storeId = CurrentStore.Id
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/delete")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult DeleteStore(string storeId)
|
||||
{
|
||||
return View("Confirm", new ConfirmModel("Delete store", "The store will be permanently deleted. This action will also delete all invoices, apps and data associated with the store. Are you sure?", "Delete"));
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/delete")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> DeleteStorePost(string storeId)
|
||||
{
|
||||
await _storeRepo.DeleteStore(CurrentStore.Id);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Store successfully deleted.";
|
||||
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/checkout")]
|
||||
public IActionResult CheckoutAppearance()
|
||||
{
|
||||
var storeBlob = CurrentStore.GetStoreBlob();
|
||||
var vm = new CheckoutAppearanceViewModel();
|
||||
SetCryptoCurrencies(vm, CurrentStore);
|
||||
vm.PaymentMethodCriteria = CurrentStore.GetPaymentMethodConfigs(_handlers)
|
||||
.Where(s => !storeBlob.GetExcludedPaymentMethods().Match(s.Key) && s.Value is not LNURLPaymentMethodConfig)
|
||||
.Select(c =>
|
||||
{
|
||||
var pmi = c.Key;
|
||||
var existing = storeBlob.PaymentMethodCriteria.SingleOrDefault(criteria =>
|
||||
criteria.PaymentMethod == pmi);
|
||||
criteria.PaymentMethod == pmi);
|
||||
return existing is null
|
||||
? new PaymentMethodCriteriaViewModel { PaymentMethod = pmi.ToString(), Value = "" }
|
||||
: new PaymentMethodCriteriaViewModel
|
||||
|
@ -246,202 +246,201 @@ namespace BTCPayServer.Controllers
|
|||
};
|
||||
}).ToList();
|
||||
|
||||
vm.UseClassicCheckout = storeBlob.CheckoutType == Client.Models.CheckoutType.V1;
|
||||
vm.CelebratePayment = storeBlob.CelebratePayment;
|
||||
vm.PlaySoundOnPayment = storeBlob.PlaySoundOnPayment;
|
||||
vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback;
|
||||
vm.ShowPayInWalletButton = storeBlob.ShowPayInWalletButton;
|
||||
vm.ShowStoreHeader = storeBlob.ShowStoreHeader;
|
||||
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
|
||||
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
|
||||
vm.LazyPaymentMethods = storeBlob.LazyPaymentMethods;
|
||||
vm.RedirectAutomatically = storeBlob.RedirectAutomatically;
|
||||
vm.CustomCSS = storeBlob.CustomCSS;
|
||||
vm.CustomLogo = storeBlob.CustomLogo;
|
||||
vm.SoundFileId = storeBlob.SoundFileId;
|
||||
vm.HtmlTitle = storeBlob.HtmlTitle;
|
||||
vm.SupportUrl = storeBlob.StoreSupportUrl;
|
||||
vm.DisplayExpirationTimer = (int)storeBlob.DisplayExpirationTimer.TotalMinutes;
|
||||
vm.ReceiptOptions = CheckoutAppearanceViewModel.ReceiptOptionsViewModel.Create(storeBlob.ReceiptOptions);
|
||||
vm.AutoDetectLanguage = storeBlob.AutoDetectLanguage;
|
||||
vm.SetLanguages(_langService, storeBlob.DefaultLang);
|
||||
vm.UseClassicCheckout = storeBlob.CheckoutType == Client.Models.CheckoutType.V1;
|
||||
vm.CelebratePayment = storeBlob.CelebratePayment;
|
||||
vm.PlaySoundOnPayment = storeBlob.PlaySoundOnPayment;
|
||||
vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback;
|
||||
vm.ShowPayInWalletButton = storeBlob.ShowPayInWalletButton;
|
||||
vm.ShowStoreHeader = storeBlob.ShowStoreHeader;
|
||||
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
|
||||
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
|
||||
vm.LazyPaymentMethods = storeBlob.LazyPaymentMethods;
|
||||
vm.RedirectAutomatically = storeBlob.RedirectAutomatically;
|
||||
vm.CustomCSS = storeBlob.CustomCSS;
|
||||
vm.CustomLogo = storeBlob.CustomLogo;
|
||||
vm.SoundFileId = storeBlob.SoundFileId;
|
||||
vm.HtmlTitle = storeBlob.HtmlTitle;
|
||||
vm.SupportUrl = storeBlob.StoreSupportUrl;
|
||||
vm.DisplayExpirationTimer = (int)storeBlob.DisplayExpirationTimer.TotalMinutes;
|
||||
vm.ReceiptOptions = CheckoutAppearanceViewModel.ReceiptOptionsViewModel.Create(storeBlob.ReceiptOptions);
|
||||
vm.AutoDetectLanguage = storeBlob.AutoDetectLanguage;
|
||||
vm.SetLanguages(_langService, storeBlob.DefaultLang);
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/checkout")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> CheckoutAppearance(CheckoutAppearanceViewModel model, [FromForm] bool RemoveSoundFile = false)
|
||||
[HttpPost("{storeId}/checkout")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> CheckoutAppearance(CheckoutAppearanceViewModel model, [FromForm] bool RemoveSoundFile = false)
|
||||
{
|
||||
bool needUpdate = false;
|
||||
var blob = CurrentStore.GetStoreBlob();
|
||||
var defaultPaymentMethodId = model.DefaultPaymentMethod == null ? null : PaymentMethodId.Parse(model.DefaultPaymentMethod);
|
||||
if (CurrentStore.GetDefaultPaymentId() != defaultPaymentMethodId)
|
||||
{
|
||||
bool needUpdate = false;
|
||||
var blob = CurrentStore.GetStoreBlob();
|
||||
var defaultPaymentMethodId = model.DefaultPaymentMethod == null ? null : PaymentMethodId.Parse(model.DefaultPaymentMethod);
|
||||
if (CurrentStore.GetDefaultPaymentId() != defaultPaymentMethodId)
|
||||
needUpdate = true;
|
||||
CurrentStore.SetDefaultPaymentId(defaultPaymentMethodId);
|
||||
}
|
||||
SetCryptoCurrencies(model, CurrentStore);
|
||||
model.SetLanguages(_langService, model.DefaultLang);
|
||||
model.PaymentMethodCriteria ??= new List<PaymentMethodCriteriaViewModel>();
|
||||
for (var index = 0; index < model.PaymentMethodCriteria.Count; index++)
|
||||
{
|
||||
var methodCriterion = model.PaymentMethodCriteria[index];
|
||||
if (!string.IsNullOrWhiteSpace(methodCriterion.Value))
|
||||
{
|
||||
needUpdate = true;
|
||||
CurrentStore.SetDefaultPaymentId(defaultPaymentMethodId);
|
||||
}
|
||||
SetCryptoCurrencies(model, CurrentStore);
|
||||
model.SetLanguages(_langService, model.DefaultLang);
|
||||
model.PaymentMethodCriteria ??= new List<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 _))
|
||||
{
|
||||
if (!CurrencyValue.TryParse(methodCriterion.Value, out _))
|
||||
{
|
||||
model.AddModelError(viewModel => viewModel.PaymentMethodCriteria[index].Value,
|
||||
$"{methodCriterion.PaymentMethod}: Invalid format. Make sure to enter a valid amount and currency code. Examples: '5 USD', '0.001 BTC'", this);
|
||||
}
|
||||
model.AddModelError(viewModel => viewModel.PaymentMethodCriteria[index].Value,
|
||||
$"{methodCriterion.PaymentMethod}: Invalid format. Make sure to enter a valid amount and currency code. Examples: '5 USD', '0.001 BTC'", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var userId = GetUserId();
|
||||
if (userId is null)
|
||||
return NotFound();
|
||||
var userId = GetUserId();
|
||||
if (userId is null)
|
||||
return NotFound();
|
||||
|
||||
if (model.SoundFile != null)
|
||||
if (model.SoundFile != null)
|
||||
{
|
||||
if (model.SoundFile.Length > 1_000_000)
|
||||
{
|
||||
if (model.SoundFile.Length > 1_000_000)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SoundFile), "The uploaded sound file should be less than 1MB");
|
||||
}
|
||||
else if (!model.SoundFile.ContentType.StartsWith("audio/", StringComparison.InvariantCulture))
|
||||
ModelState.AddModelError(nameof(model.SoundFile), "The uploaded sound file should be less than 1MB");
|
||||
}
|
||||
else if (!model.SoundFile.ContentType.StartsWith("audio/", StringComparison.InvariantCulture))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SoundFile), "The uploaded sound file needs to be an audio file");
|
||||
}
|
||||
else
|
||||
{
|
||||
var formFile = await model.SoundFile.Bufferize();
|
||||
if (!FileTypeDetector.IsAudio(formFile.Buffer, formFile.FileName))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SoundFile), "The uploaded sound file needs to be an audio file");
|
||||
}
|
||||
else
|
||||
{
|
||||
var formFile = await model.SoundFile.Bufferize();
|
||||
if (!FileTypeDetector.IsAudio(formFile.Buffer, formFile.FileName))
|
||||
model.SoundFile = formFile;
|
||||
// delete existing file
|
||||
if (!string.IsNullOrEmpty(blob.SoundFileId))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SoundFile), "The uploaded sound file needs to be an audio file");
|
||||
await _fileService.RemoveFile(blob.SoundFileId, userId);
|
||||
}
|
||||
else
|
||||
{
|
||||
model.SoundFile = formFile;
|
||||
// delete existing file
|
||||
if (!string.IsNullOrEmpty(blob.SoundFileId))
|
||||
{
|
||||
await _fileService.RemoveFile(blob.SoundFileId, userId);
|
||||
}
|
||||
|
||||
// add new file
|
||||
try
|
||||
{
|
||||
var storedFile = await _fileService.AddFile(model.SoundFile, userId);
|
||||
blob.SoundFileId = storedFile.Id;
|
||||
needUpdate = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SoundFile), $"Could not save sound: {e.Message}");
|
||||
}
|
||||
// add new file
|
||||
try
|
||||
{
|
||||
var storedFile = await _fileService.AddFile(model.SoundFile, userId);
|
||||
blob.SoundFileId = storedFile.Id;
|
||||
needUpdate = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SoundFile), $"Could not save sound: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (RemoveSoundFile && !string.IsNullOrEmpty(blob.SoundFileId))
|
||||
{
|
||||
await _fileService.RemoveFile(blob.SoundFileId, userId);
|
||||
blob.SoundFileId = null;
|
||||
needUpdate = true;
|
||||
}
|
||||
}
|
||||
else if (RemoveSoundFile && !string.IsNullOrEmpty(blob.SoundFileId))
|
||||
{
|
||||
await _fileService.RemoveFile(blob.SoundFileId, userId);
|
||||
blob.SoundFileId = null;
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// Payment criteria for Off-Chain should also affect LNUrl
|
||||
foreach (var newCriteria in model.PaymentMethodCriteria.ToList())
|
||||
{
|
||||
var paymentMethodId = PaymentMethodId.Parse(newCriteria.PaymentMethod);
|
||||
if (_handlers.TryGet(paymentMethodId) is LightningLikePaymentHandler h)
|
||||
model.PaymentMethodCriteria.Add(new PaymentMethodCriteriaViewModel
|
||||
{
|
||||
PaymentMethod = PaymentTypes.LNURL.GetPaymentMethodId(h.Network.CryptoCode).ToString(),
|
||||
Type = newCriteria.Type,
|
||||
Value = newCriteria.Value
|
||||
});
|
||||
// Should not be able to set LNUrlPay criteria directly in UI
|
||||
if (_handlers.TryGet(paymentMethodId) is LNURLPayPaymentHandler)
|
||||
model.PaymentMethodCriteria.Remove(newCriteria);
|
||||
}
|
||||
blob.PaymentMethodCriteria ??= new List<PaymentMethodCriteria>();
|
||||
foreach (var newCriteria in model.PaymentMethodCriteria)
|
||||
{
|
||||
var paymentMethodId = PaymentMethodId.Parse(newCriteria.PaymentMethod);
|
||||
var existingCriteria = blob.PaymentMethodCriteria.FirstOrDefault(c => c.PaymentMethod == paymentMethodId);
|
||||
if (existingCriteria != null)
|
||||
blob.PaymentMethodCriteria.Remove(existingCriteria);
|
||||
CurrencyValue.TryParse(newCriteria.Value, out var cv);
|
||||
blob.PaymentMethodCriteria.Add(new PaymentMethodCriteria()
|
||||
// Payment criteria for Off-Chain should also affect LNUrl
|
||||
foreach (var newCriteria in model.PaymentMethodCriteria.ToList())
|
||||
{
|
||||
var paymentMethodId = PaymentMethodId.Parse(newCriteria.PaymentMethod);
|
||||
if (_handlers.TryGet(paymentMethodId) is LightningLikePaymentHandler h)
|
||||
model.PaymentMethodCriteria.Add(new PaymentMethodCriteriaViewModel
|
||||
{
|
||||
Above = newCriteria.Type == PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan,
|
||||
Value = cv,
|
||||
PaymentMethod = paymentMethodId
|
||||
PaymentMethod = PaymentTypes.LNURL.GetPaymentMethodId(h.Network.CryptoCode).ToString(),
|
||||
Type = newCriteria.Type,
|
||||
Value = newCriteria.Value
|
||||
});
|
||||
}
|
||||
|
||||
blob.ShowPayInWalletButton = model.ShowPayInWalletButton;
|
||||
blob.ShowStoreHeader = model.ShowStoreHeader;
|
||||
blob.CheckoutType = model.UseClassicCheckout ? Client.Models.CheckoutType.V1 : Client.Models.CheckoutType.V2;
|
||||
blob.CelebratePayment = model.CelebratePayment;
|
||||
blob.PlaySoundOnPayment = model.PlaySoundOnPayment;
|
||||
blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback;
|
||||
blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi;
|
||||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||
blob.LazyPaymentMethods = model.LazyPaymentMethods;
|
||||
blob.RedirectAutomatically = model.RedirectAutomatically;
|
||||
blob.ReceiptOptions = model.ReceiptOptions.ToDTO();
|
||||
blob.CustomLogo = model.CustomLogo;
|
||||
blob.CustomCSS = model.CustomCSS;
|
||||
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;
|
||||
blob.StoreSupportUrl = string.IsNullOrWhiteSpace(model.SupportUrl) ? null : model.SupportUrl.IsValidEmail() ? $"mailto:{model.SupportUrl}" : model.SupportUrl;
|
||||
blob.DisplayExpirationTimer = TimeSpan.FromMinutes(model.DisplayExpirationTimer);
|
||||
blob.AutoDetectLanguage = model.AutoDetectLanguage;
|
||||
blob.DefaultLang = model.DefaultLang;
|
||||
blob.NormalizeToRelativeLinks(Request);
|
||||
if (CurrentStore.SetStoreBlob(blob))
|
||||
// Should not be able to set LNUrlPay criteria directly in UI
|
||||
if (_handlers.TryGet(paymentMethodId) is LNURLPayPaymentHandler)
|
||||
model.PaymentMethodCriteria.Remove(newCriteria);
|
||||
}
|
||||
blob.PaymentMethodCriteria ??= new List<PaymentMethodCriteria>();
|
||||
foreach (var newCriteria in model.PaymentMethodCriteria)
|
||||
{
|
||||
var paymentMethodId = PaymentMethodId.Parse(newCriteria.PaymentMethod);
|
||||
var existingCriteria = blob.PaymentMethodCriteria.FirstOrDefault(c => c.PaymentMethod == paymentMethodId);
|
||||
if (existingCriteria != null)
|
||||
blob.PaymentMethodCriteria.Remove(existingCriteria);
|
||||
CurrencyValue.TryParse(newCriteria.Value, out var cv);
|
||||
blob.PaymentMethodCriteria.Add(new PaymentMethodCriteria()
|
||||
{
|
||||
needUpdate = true;
|
||||
}
|
||||
if (needUpdate)
|
||||
{
|
||||
await _storeRepo.UpdateStore(CurrentStore);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(CheckoutAppearance), new
|
||||
{
|
||||
storeId = CurrentStore.Id
|
||||
Above = newCriteria.Type == PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan,
|
||||
Value = cv,
|
||||
PaymentMethod = paymentMethodId
|
||||
});
|
||||
}
|
||||
|
||||
void SetCryptoCurrencies(CheckoutAppearanceViewModel vm, StoreData storeData)
|
||||
blob.ShowPayInWalletButton = model.ShowPayInWalletButton;
|
||||
blob.ShowStoreHeader = model.ShowStoreHeader;
|
||||
blob.CheckoutType = model.UseClassicCheckout ? Client.Models.CheckoutType.V1 : Client.Models.CheckoutType.V2;
|
||||
blob.CelebratePayment = model.CelebratePayment;
|
||||
blob.PlaySoundOnPayment = model.PlaySoundOnPayment;
|
||||
blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback;
|
||||
blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi;
|
||||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||
blob.LazyPaymentMethods = model.LazyPaymentMethods;
|
||||
blob.RedirectAutomatically = model.RedirectAutomatically;
|
||||
blob.ReceiptOptions = model.ReceiptOptions.ToDTO();
|
||||
blob.CustomLogo = model.CustomLogo;
|
||||
blob.CustomCSS = model.CustomCSS;
|
||||
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;
|
||||
blob.StoreSupportUrl = string.IsNullOrWhiteSpace(model.SupportUrl) ? null : model.SupportUrl.IsValidEmail() ? $"mailto:{model.SupportUrl}" : model.SupportUrl;
|
||||
blob.DisplayExpirationTimer = TimeSpan.FromMinutes(model.DisplayExpirationTimer);
|
||||
blob.AutoDetectLanguage = model.AutoDetectLanguage;
|
||||
blob.DefaultLang = model.DefaultLang;
|
||||
blob.NormalizeToRelativeLinks(Request);
|
||||
if (CurrentStore.SetStoreBlob(blob))
|
||||
{
|
||||
var choices = GetEnabledPaymentMethodChoices(storeData);
|
||||
var chosen = GetDefaultPaymentMethodChoice(storeData);
|
||||
|
||||
vm.PaymentMethods = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen?.Value);
|
||||
vm.DefaultPaymentMethod = chosen?.Value;
|
||||
needUpdate = true;
|
||||
}
|
||||
if (needUpdate)
|
||||
{
|
||||
await _storeRepo.UpdateStore(CurrentStore);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated";
|
||||
}
|
||||
|
||||
PaymentMethodOptionViewModel.Format? GetDefaultPaymentMethodChoice(StoreData storeData)
|
||||
return RedirectToAction(nameof(CheckoutAppearance), new
|
||||
{
|
||||
var enabled = storeData.GetEnabledPaymentIds();
|
||||
var defaultPaymentId = storeData.GetDefaultPaymentId();
|
||||
var defaultChoice = defaultPaymentId?.FindNearest(enabled);
|
||||
if (defaultChoice is null)
|
||||
{
|
||||
defaultChoice = enabled.FirstOrDefault(e => e == PaymentTypes.CHAIN.GetPaymentMethodId(_networkProvider.DefaultNetwork.CryptoCode)) ??
|
||||
enabled.FirstOrDefault(e => e == PaymentTypes.LN.GetPaymentMethodId(_networkProvider.DefaultNetwork.CryptoCode)) ??
|
||||
enabled.FirstOrDefault();
|
||||
}
|
||||
var choices = GetEnabledPaymentMethodChoices(storeData);
|
||||
storeId = CurrentStore.Id
|
||||
});
|
||||
}
|
||||
|
||||
return defaultChoice is null ? null : choices.FirstOrDefault(c => defaultChoice.ToString().Equals(c.Value, StringComparison.OrdinalIgnoreCase));
|
||||
void SetCryptoCurrencies(CheckoutAppearanceViewModel vm, StoreData storeData)
|
||||
{
|
||||
var choices = GetEnabledPaymentMethodChoices(storeData);
|
||||
var chosen = GetDefaultPaymentMethodChoice(storeData);
|
||||
|
||||
vm.PaymentMethods = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen?.Value);
|
||||
vm.DefaultPaymentMethod = chosen?.Value;
|
||||
}
|
||||
|
||||
PaymentMethodOptionViewModel.Format? GetDefaultPaymentMethodChoice(StoreData storeData)
|
||||
{
|
||||
var enabled = storeData.GetEnabledPaymentIds();
|
||||
var defaultPaymentId = storeData.GetDefaultPaymentId();
|
||||
var defaultChoice = defaultPaymentId?.FindNearest(enabled);
|
||||
if (defaultChoice is null)
|
||||
{
|
||||
defaultChoice = enabled.FirstOrDefault(e => e == PaymentTypes.CHAIN.GetPaymentMethodId(_networkProvider.DefaultNetwork.CryptoCode)) ??
|
||||
enabled.FirstOrDefault(e => e == PaymentTypes.LN.GetPaymentMethodId(_networkProvider.DefaultNetwork.CryptoCode)) ??
|
||||
enabled.FirstOrDefault();
|
||||
}
|
||||
var choices = GetEnabledPaymentMethodChoices(storeData);
|
||||
|
||||
return defaultChoice is null ? null : choices.FirstOrDefault(c => defaultChoice.ToString().Equals(c.Value, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,249 +14,248 @@ using Microsoft.AspNetCore.Mvc.Rendering;
|
|||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
namespace BTCPayServer.Controllers;
|
||||
|
||||
public partial class UIStoresController
|
||||
{
|
||||
public partial class UIStoresController
|
||||
[HttpGet("{storeId}/tokens")]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ListTokens()
|
||||
{
|
||||
[HttpGet("{storeId}/tokens")]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ListTokens()
|
||||
var model = new TokensViewModel();
|
||||
var tokens = await _tokenRepository.GetTokensByStoreIdAsync(CurrentStore.Id);
|
||||
model.StoreNotConfigured = StoreNotConfigured;
|
||||
model.Tokens = tokens.Select(t => new TokenViewModel()
|
||||
{
|
||||
var model = new TokensViewModel();
|
||||
var tokens = await _tokenRepository.GetTokensByStoreIdAsync(CurrentStore.Id);
|
||||
model.StoreNotConfigured = StoreNotConfigured;
|
||||
model.Tokens = tokens.Select(t => new TokenViewModel()
|
||||
{
|
||||
Label = t.Label,
|
||||
SIN = t.SIN,
|
||||
Id = t.Value
|
||||
}).ToArray();
|
||||
|
||||
model.ApiKey = (await _tokenRepository.GetLegacyAPIKeys(CurrentStore.Id)).FirstOrDefault();
|
||||
model.EncodedApiKey = model.ApiKey == null ? "*API Key*" : Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(model.ApiKey));
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/tokens/{tokenId}/revoke")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> 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 <strong>{_html.Encode(token.Label)}</strong> will be revoked. Do you wish to continue?", "Revoke"));
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/tokens/{tokenId}/revoke")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> CreateToken(string storeId, CreateTokenViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(nameof(CreateToken), model);
|
||||
}
|
||||
model.Label ??= string.Empty;
|
||||
var userId = GetUserId();
|
||||
if (userId == null)
|
||||
return Challenge(AuthenticationSchemes.Cookie);
|
||||
var store = model.StoreId switch
|
||||
{
|
||||
null => CurrentStore,
|
||||
_ => await _storeRepo.FindStore(storeId, userId)
|
||||
};
|
||||
if (store == null)
|
||||
return Challenge(AuthenticationSchemes.Cookie);
|
||||
var tokenRequest = new TokenRequest()
|
||||
Label = t.Label,
|
||||
SIN = t.SIN,
|
||||
Id = t.Value
|
||||
}).ToArray();
|
||||
|
||||
model.ApiKey = (await _tokenRepository.GetLegacyAPIKeys(CurrentStore.Id)).FirstOrDefault();
|
||||
model.EncodedApiKey = model.ApiKey == null ? "*API Key*" : Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(model.ApiKey));
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/tokens/{tokenId}/revoke")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> 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 <strong>{_html.Encode(token.Label)}</strong> will be revoked. Do you wish to continue?", "Revoke"));
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/tokens/{tokenId}/revoke")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> CreateToken(string storeId, CreateTokenViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(nameof(CreateToken), model);
|
||||
}
|
||||
model.Label ??= string.Empty;
|
||||
var userId = GetUserId();
|
||||
if (userId == null)
|
||||
return Challenge(AuthenticationSchemes.Cookie);
|
||||
var store = model.StoreId switch
|
||||
{
|
||||
null => CurrentStore,
|
||||
_ => await _storeRepo.FindStore(storeId, userId)
|
||||
};
|
||||
if (store == null)
|
||||
return Challenge(AuthenticationSchemes.Cookie);
|
||||
var tokenRequest = new TokenRequest()
|
||||
{
|
||||
Label = model.Label,
|
||||
Id = model.PublicKey == null ? null : NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(new PubKey(model.PublicKey).Compress())
|
||||
};
|
||||
|
||||
string? pairingCode;
|
||||
if (model.PublicKey == null)
|
||||
{
|
||||
tokenRequest.PairingCode = await _tokenRepository.CreatePairingCodeAsync();
|
||||
await _tokenRepository.UpdatePairingCode(new PairingCodeEntity()
|
||||
{
|
||||
Id = tokenRequest.PairingCode,
|
||||
Label = model.Label,
|
||||
Id = model.PublicKey == null ? null : NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(new PubKey(model.PublicKey).Compress())
|
||||
};
|
||||
|
||||
string? pairingCode;
|
||||
if (model.PublicKey == null)
|
||||
{
|
||||
tokenRequest.PairingCode = await _tokenRepository.CreatePairingCodeAsync();
|
||||
await _tokenRepository.UpdatePairingCode(new PairingCodeEntity()
|
||||
{
|
||||
Id = tokenRequest.PairingCode,
|
||||
Label = model.Label,
|
||||
});
|
||||
await _tokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, store.Id);
|
||||
pairingCode = tokenRequest.PairingCode;
|
||||
}
|
||||
else
|
||||
{
|
||||
pairingCode = (await _tokenController.Tokens(tokenRequest)).Data[0].PairingCode;
|
||||
}
|
||||
|
||||
GeneratedPairingCode = pairingCode;
|
||||
return RedirectToAction(nameof(RequestPairing), new
|
||||
{
|
||||
pairingCode,
|
||||
selectedStore = storeId
|
||||
});
|
||||
await _tokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, store.Id);
|
||||
pairingCode = tokenRequest.PairingCode;
|
||||
}
|
||||
else
|
||||
{
|
||||
pairingCode = (await _tokenController.Tokens(tokenRequest)).Data[0].PairingCode;
|
||||
}
|
||||
|
||||
[HttpGet("/api-tokens")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> CreateToken()
|
||||
GeneratedPairingCode = pairingCode;
|
||||
return RedirectToAction(nameof(RequestPairing), new
|
||||
{
|
||||
var userId = GetUserId();
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
return Challenge(AuthenticationSchemes.Cookie);
|
||||
var model = new CreateTokenViewModel();
|
||||
ViewBag.HidePublicKey = true;
|
||||
ViewBag.ShowStores = true;
|
||||
ViewBag.ShowMenu = false;
|
||||
var stores = (await _storeRepo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray();
|
||||
pairingCode,
|
||||
selectedStore = storeId
|
||||
});
|
||||
}
|
||||
|
||||
model.Stores = new SelectList(stores, nameof(CurrentStore.Id), nameof(CurrentStore.StoreName));
|
||||
if (!model.Stores.Any())
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "You need to be owner of at least one store before pairing";
|
||||
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
|
||||
}
|
||||
return View(model);
|
||||
[HttpGet("/api-tokens")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> CreateToken()
|
||||
{
|
||||
var userId = GetUserId();
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
return Challenge(AuthenticationSchemes.Cookie);
|
||||
var model = new CreateTokenViewModel();
|
||||
ViewBag.HidePublicKey = true;
|
||||
ViewBag.ShowStores = true;
|
||||
ViewBag.ShowMenu = false;
|
||||
var stores = (await _storeRepo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray();
|
||||
|
||||
model.Stores = new SelectList(stores, nameof(CurrentStore.Id), nameof(CurrentStore.StoreName));
|
||||
if (!model.Stores.Any())
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "You need to be owner of at least one store before pairing";
|
||||
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost("/api-tokens")]
|
||||
[AllowAnonymous]
|
||||
public Task<IActionResult> CreateToken2(CreateTokenViewModel model)
|
||||
{
|
||||
return CreateToken(model.StoreId, model);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/tokens/apikey")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> GenerateAPIKey(string storeId, string command = "")
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
if (command == "revoke")
|
||||
{
|
||||
await _tokenRepository.RevokeLegacyAPIKeys(CurrentStore.Id);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "API Key revoked";
|
||||
}
|
||||
else
|
||||
{
|
||||
await _tokenRepository.GenerateLegacyAPIKey(CurrentStore.Id);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "API Key re-generated";
|
||||
}
|
||||
|
||||
[HttpPost("/api-tokens")]
|
||||
[AllowAnonymous]
|
||||
public Task<IActionResult> CreateToken2(CreateTokenViewModel model)
|
||||
return RedirectToAction(nameof(ListTokens), new
|
||||
{
|
||||
return CreateToken(model.StoreId, model);
|
||||
}
|
||||
storeId
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/tokens/apikey")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> GenerateAPIKey(string storeId, string command = "")
|
||||
[HttpGet("/api-access-request")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> RequestPairing(string pairingCode, string? selectedStore = null)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
if (userId == null)
|
||||
return Challenge(AuthenticationSchemes.Cookie);
|
||||
|
||||
if (pairingCode == null)
|
||||
return NotFound();
|
||||
|
||||
if (selectedStore != null)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
var store = await _storeRepo.FindStore(selectedStore, userId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
if (command == "revoke")
|
||||
{
|
||||
await _tokenRepository.RevokeLegacyAPIKeys(CurrentStore.Id);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "API Key revoked";
|
||||
}
|
||||
else
|
||||
{
|
||||
await _tokenRepository.GenerateLegacyAPIKey(CurrentStore.Id);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "API Key re-generated";
|
||||
}
|
||||
HttpContext.SetStoreData(store);
|
||||
}
|
||||
|
||||
var pairing = await _tokenRepository.GetPairingAsync(pairingCode);
|
||||
if (pairing == null)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Unknown pairing code";
|
||||
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
|
||||
}
|
||||
|
||||
var stores = (await _storeRepo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray();
|
||||
return View(new PairingModel
|
||||
{
|
||||
Id = pairing.Id,
|
||||
Label = pairing.Label,
|
||||
SIN = pairing.SIN ?? "Server-Initiated Pairing",
|
||||
StoreId = selectedStore ?? stores.FirstOrDefault()?.Id,
|
||||
Stores = stores.Select(s => new PairingModel.StoreViewModel
|
||||
{
|
||||
Id = s.Id,
|
||||
Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName
|
||||
}).ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("/api-access-request")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Pair(string pairingCode, string storeId)
|
||||
{
|
||||
if (pairingCode == null)
|
||||
return NotFound();
|
||||
var store = CurrentStore;
|
||||
var pairing = await _tokenRepository.GetPairingAsync(pairingCode);
|
||||
if (store == null || pairing == null)
|
||||
return NotFound();
|
||||
|
||||
var pairingResult = await _tokenRepository.PairWithStoreAsync(pairingCode, store.Id);
|
||||
if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
|
||||
{
|
||||
var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods();
|
||||
StoreNotConfigured = store.GetPaymentMethodConfigs(_handlers).All(p => excludeFilter.Match(p.Key));
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Pairing is successful";
|
||||
if (pairingResult == PairingResult.Partial)
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Server initiated pairing code: " + pairingCode;
|
||||
return RedirectToAction(nameof(ListTokens), new
|
||||
{
|
||||
storeId
|
||||
storeId = store.Id, pairingCode
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("/api-access-request")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> RequestPairing(string pairingCode, string? selectedStore = null)
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"Pairing failed ({pairingResult})";
|
||||
return RedirectToAction(nameof(ListTokens), new
|
||||
{
|
||||
var userId = GetUserId();
|
||||
if (userId == null)
|
||||
return Challenge(AuthenticationSchemes.Cookie);
|
||||
|
||||
if (pairingCode == null)
|
||||
return NotFound();
|
||||
|
||||
if (selectedStore != null)
|
||||
{
|
||||
var store = await _storeRepo.FindStore(selectedStore, userId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
HttpContext.SetStoreData(store);
|
||||
}
|
||||
|
||||
var pairing = await _tokenRepository.GetPairingAsync(pairingCode);
|
||||
if (pairing == null)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Unknown pairing code";
|
||||
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
|
||||
}
|
||||
|
||||
var stores = (await _storeRepo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray();
|
||||
return View(new PairingModel
|
||||
{
|
||||
Id = pairing.Id,
|
||||
Label = pairing.Label,
|
||||
SIN = pairing.SIN ?? "Server-Initiated Pairing",
|
||||
StoreId = selectedStore ?? stores.FirstOrDefault()?.Id,
|
||||
Stores = stores.Select(s => new PairingModel.StoreViewModel
|
||||
{
|
||||
Id = s.Id,
|
||||
Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName
|
||||
}).ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("/api-access-request")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Pair(string pairingCode, string storeId)
|
||||
{
|
||||
if (pairingCode == null)
|
||||
return NotFound();
|
||||
var store = CurrentStore;
|
||||
var pairing = await _tokenRepository.GetPairingAsync(pairingCode);
|
||||
if (store == null || pairing == null)
|
||||
return NotFound();
|
||||
|
||||
var pairingResult = await _tokenRepository.PairWithStoreAsync(pairingCode, store.Id);
|
||||
if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
|
||||
{
|
||||
var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods();
|
||||
StoreNotConfigured = store.GetPaymentMethodConfigs(_handlers).All(p => excludeFilter.Match(p.Key));
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Pairing is successful";
|
||||
if (pairingResult == PairingResult.Partial)
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Server initiated pairing code: " + pairingCode;
|
||||
return RedirectToAction(nameof(ListTokens), new
|
||||
{
|
||||
storeId = store.Id, pairingCode
|
||||
});
|
||||
}
|
||||
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"Pairing failed ({pairingResult})";
|
||||
return RedirectToAction(nameof(ListTokens), new
|
||||
{
|
||||
storeId = store.Id
|
||||
});
|
||||
}
|
||||
storeId = store.Id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,135 +14,134 @@ using BTCPayServer.Services.Stores;
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
namespace BTCPayServer.Controllers;
|
||||
|
||||
public partial class UIStoresController
|
||||
{
|
||||
public partial class UIStoresController
|
||||
[HttpGet("{storeId}/users")]
|
||||
public async Task<IActionResult> StoreUsers()
|
||||
{
|
||||
[HttpGet("{storeId}/users")]
|
||||
public async Task<IActionResult> StoreUsers()
|
||||
var vm = new StoreUsersViewModel { Role = StoreRoleId.Employee.Role };
|
||||
await FillUsers(vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/users")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> StoreUsers(string storeId, StoreUsersViewModel vm)
|
||||
{
|
||||
await FillUsers(vm);
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
var vm = new StoreUsersViewModel { Role = StoreRoleId.Employee.Role };
|
||||
await FillUsers(vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/users")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> StoreUsers(string storeId, StoreUsersViewModel vm)
|
||||
var roles = await _storeRepo.GetStoreRoles(CurrentStore.Id);
|
||||
if (roles.All(role => role.Id != vm.Role))
|
||||
{
|
||||
await FillUsers(vm);
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var roles = await _storeRepo.GetStoreRoles(CurrentStore.Id);
|
||||
if (roles.All(role => role.Id != vm.Role))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Role), "Invalid role");
|
||||
return View(vm);
|
||||
}
|
||||
ModelState.AddModelError(nameof(vm.Role), "Invalid role");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByEmailAsync(vm.Email);
|
||||
var isExistingUser = user is not null;
|
||||
var isExistingStoreUser = isExistingUser && await _storeRepo.GetStoreUser(storeId, user!.Id) is not null;
|
||||
var successInfo = string.Empty;
|
||||
if (user == null)
|
||||
var user = await _userManager.FindByEmailAsync(vm.Email);
|
||||
var isExistingUser = user is not null;
|
||||
var isExistingStoreUser = isExistingUser && await _storeRepo.GetStoreUser(storeId, user!.Id) is not null;
|
||||
var successInfo = string.Empty;
|
||||
if (user == null)
|
||||
{
|
||||
user = new ApplicationUser
|
||||
{
|
||||
user = new ApplicationUser
|
||||
{
|
||||
UserName = vm.Email,
|
||||
Email = vm.Email,
|
||||
RequiresEmailConfirmation = _policiesSettings.RequiresConfirmedEmail,
|
||||
RequiresApproval = _policiesSettings.RequiresUserApproval,
|
||||
Created = DateTimeOffset.UtcNow
|
||||
};
|
||||
UserName = vm.Email,
|
||||
Email = vm.Email,
|
||||
RequiresEmailConfirmation = _policiesSettings.RequiresConfirmedEmail,
|
||||
RequiresApproval = _policiesSettings.RequiresUserApproval,
|
||||
Created = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
var result = await _userManager.CreateAsync(user);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<Uri>();
|
||||
var currentUser = await _userManager.GetUserAsync(HttpContext.User);
|
||||
|
||||
_eventAggregator.Publish(new UserRegisteredEvent
|
||||
{
|
||||
RequestUri = Request.GetAbsoluteRootUri(),
|
||||
Kind = UserRegisteredEventKind.Invite,
|
||||
User = user,
|
||||
InvitedByUser = currentUser,
|
||||
CallbackUrlGenerated = tcs
|
||||
});
|
||||
|
||||
var callbackUrl = await tcs.Task;
|
||||
var settings = await _settingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
||||
var info = settings.IsComplete()
|
||||
? "An invitation email has been sent.<br/>You may alternatively"
|
||||
: "An invitation email has not been sent, because the server does not have an email server configured.<br/> You need to";
|
||||
successInfo = $"{info} share this link with them: <a class='alert-link' href='{callbackUrl}'>{callbackUrl}</a>";
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Email), "User could not be invited");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
var roleId = await _storeRepo.ResolveStoreRoleId(storeId, vm.Role);
|
||||
var action = isExistingUser
|
||||
? isExistingStoreUser ? "updated" : "added"
|
||||
: "invited";
|
||||
if (await _storeRepo.AddOrUpdateStoreUser(CurrentStore.Id, user.Id, roleId))
|
||||
var result = await _userManager.CreateAsync(user);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
var tcs = new TaskCompletionSource<Uri>();
|
||||
var currentUser = await _userManager.GetUserAsync(HttpContext.User);
|
||||
|
||||
_eventAggregator.Publish(new UserRegisteredEvent
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
AllowDismiss = false,
|
||||
Html = $"User {action} successfully." + (string.IsNullOrEmpty(successInfo) ? "" : $" {successInfo}")
|
||||
RequestUri = Request.GetAbsoluteRootUri(),
|
||||
Kind = UserRegisteredEventKind.Invite,
|
||||
User = user,
|
||||
InvitedByUser = currentUser,
|
||||
CallbackUrlGenerated = tcs
|
||||
});
|
||||
return RedirectToAction(nameof(StoreUsers));
|
||||
|
||||
var callbackUrl = await tcs.Task;
|
||||
var settings = await _settingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
||||
var info = settings.IsComplete()
|
||||
? "An invitation email has been sent.<br/>You may alternatively"
|
||||
: "An invitation email has not been sent, because the server does not have an email server configured.<br/> You need to";
|
||||
successInfo = $"{info} share this link with them: <a class='alert-link' href='{callbackUrl}'>{callbackUrl}</a>";
|
||||
}
|
||||
|
||||
ModelState.AddModelError(nameof(vm.Email), $"The user could not be {action}");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/users/{userId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> UpdateStoreUser(string storeId, string userId, StoreUsersViewModel.StoreUserViewModel vm)
|
||||
{
|
||||
var roleId = await _storeRepo.ResolveStoreRoleId(storeId, vm.Role);
|
||||
var storeUsers = await _storeRepo.GetStoreUsers(storeId);
|
||||
var user = storeUsers.First(user => user.Id == userId);
|
||||
var isOwner = user.StoreRole.Id == StoreRoleId.Owner.Id;
|
||||
var isLastOwner = isOwner && storeUsers.Count(u => u.StoreRole.Id == StoreRoleId.Owner.Id) == 1;
|
||||
if (isLastOwner && roleId != StoreRoleId.Owner)
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"User {user.Email} is the last owner. Their role cannot be changed.";
|
||||
else if (await _storeRepo.AddOrUpdateStoreUser(storeId, userId, roleId))
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"The role of {user.Email} has been changed to {vm.Role}.";
|
||||
return RedirectToAction(nameof(StoreUsers), new { storeId, userId });
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/users/{userId}/delete")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> DeleteStoreUser(string storeId, string userId)
|
||||
{
|
||||
if (await _storeRepo.RemoveStoreUser(storeId, userId))
|
||||
TempData[WellKnownTempData.SuccessMessage] = "User removed successfully.";
|
||||
else
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Removing this user would result in the store having no owner.";
|
||||
return RedirectToAction(nameof(StoreUsers), new { storeId, userId });
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Email), "User could not be invited");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FillUsers(StoreUsersViewModel vm)
|
||||
var roleId = await _storeRepo.ResolveStoreRoleId(storeId, vm.Role);
|
||||
var action = isExistingUser
|
||||
? isExistingStoreUser ? "updated" : "added"
|
||||
: "invited";
|
||||
if (await _storeRepo.AddOrUpdateStoreUser(CurrentStore.Id, user.Id, roleId))
|
||||
{
|
||||
var users = await _storeRepo.GetStoreUsers(CurrentStore.Id);
|
||||
vm.StoreId = CurrentStore.Id;
|
||||
vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel()
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Email = u.Email,
|
||||
Id = u.Id,
|
||||
Role = u.StoreRole.Role
|
||||
}).ToList();
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
AllowDismiss = false,
|
||||
Html = $"User {action} successfully." + (string.IsNullOrEmpty(successInfo) ? "" : $" {successInfo}")
|
||||
});
|
||||
return RedirectToAction(nameof(StoreUsers));
|
||||
}
|
||||
|
||||
ModelState.AddModelError(nameof(vm.Email), $"The user could not be {action}");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/users/{userId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> UpdateStoreUser(string storeId, string userId, StoreUsersViewModel.StoreUserViewModel vm)
|
||||
{
|
||||
var roleId = await _storeRepo.ResolveStoreRoleId(storeId, vm.Role);
|
||||
var storeUsers = await _storeRepo.GetStoreUsers(storeId);
|
||||
var user = storeUsers.First(user => user.Id == userId);
|
||||
var isOwner = user.StoreRole.Id == StoreRoleId.Owner.Id;
|
||||
var isLastOwner = isOwner && storeUsers.Count(u => u.StoreRole.Id == StoreRoleId.Owner.Id) == 1;
|
||||
if (isLastOwner && roleId != StoreRoleId.Owner)
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"User {user.Email} is the last owner. Their role cannot be changed.";
|
||||
else if (await _storeRepo.AddOrUpdateStoreUser(storeId, userId, roleId))
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"The role of {user.Email} has been changed to {vm.Role}.";
|
||||
return RedirectToAction(nameof(StoreUsers), new { storeId, userId });
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/users/{userId}/delete")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> DeleteStoreUser(string storeId, string userId)
|
||||
{
|
||||
if (await _storeRepo.RemoveStoreUser(storeId, userId))
|
||||
TempData[WellKnownTempData.SuccessMessage] = "User removed successfully.";
|
||||
else
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Removing this user would result in the store having no owner.";
|
||||
return RedirectToAction(nameof(StoreUsers), new { storeId, userId });
|
||||
}
|
||||
|
||||
private async Task FillUsers(StoreUsersViewModel vm)
|
||||
{
|
||||
var users = await _storeRepo.GetStoreUsers(CurrentStore.Id);
|
||||
vm.StoreId = CurrentStore.Id;
|
||||
vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel()
|
||||
{
|
||||
Email = u.Email,
|
||||
Id = u.Id,
|
||||
Role = u.StoreRole.Role
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,141 +24,140 @@ using Microsoft.AspNetCore.Mvc.Rendering;
|
|||
using Microsoft.Extensions.Options;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
namespace BTCPayServer.Controllers;
|
||||
|
||||
[Route("stores")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public partial class UIStoresController : Controller
|
||||
{
|
||||
[Route("stores")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public partial class UIStoresController : Controller
|
||||
public UIStoresController(
|
||||
BTCPayServerOptions btcpayServerOptions,
|
||||
BTCPayServerEnvironment btcpayEnv,
|
||||
StoreRepository storeRepo,
|
||||
TokenRepository tokenRepo,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
BitpayAccessTokenController tokenController,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
RateFetcher rateFactory,
|
||||
ExplorerClientProvider explorerProvider,
|
||||
LanguageService langService,
|
||||
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
|
||||
PoliciesSettings policiesSettings,
|
||||
IAuthorizationService authorizationService,
|
||||
AppService appService,
|
||||
IFileService fileService,
|
||||
WebhookSender webhookNotificationManager,
|
||||
IDataProtectionProvider dataProtector,
|
||||
IOptions<LightningNetworkOptions> lightningNetworkOptions,
|
||||
IOptions<ExternalServicesOptions> externalServiceOptions,
|
||||
IHtmlHelper html,
|
||||
EmailSenderFactory emailSenderFactory,
|
||||
WalletFileParsers onChainWalletParsers,
|
||||
SettingsRepository settingsRepository,
|
||||
EventAggregator eventAggregator)
|
||||
{
|
||||
public UIStoresController(
|
||||
BTCPayServerOptions btcpayServerOptions,
|
||||
BTCPayServerEnvironment btcpayEnv,
|
||||
StoreRepository storeRepo,
|
||||
TokenRepository tokenRepo,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
BitpayAccessTokenController tokenController,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
RateFetcher rateFactory,
|
||||
ExplorerClientProvider explorerProvider,
|
||||
LanguageService langService,
|
||||
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
|
||||
PoliciesSettings policiesSettings,
|
||||
IAuthorizationService authorizationService,
|
||||
AppService appService,
|
||||
IFileService fileService,
|
||||
WebhookSender webhookNotificationManager,
|
||||
IDataProtectionProvider dataProtector,
|
||||
IOptions<LightningNetworkOptions> lightningNetworkOptions,
|
||||
IOptions<ExternalServicesOptions> externalServiceOptions,
|
||||
IHtmlHelper html,
|
||||
EmailSenderFactory emailSenderFactory,
|
||||
WalletFileParsers onChainWalletParsers,
|
||||
SettingsRepository settingsRepository,
|
||||
EventAggregator eventAggregator)
|
||||
{
|
||||
_rateFactory = rateFactory;
|
||||
_storeRepo = storeRepo;
|
||||
_tokenRepository = tokenRepo;
|
||||
_userManager = userManager;
|
||||
_langService = langService;
|
||||
_tokenController = tokenController;
|
||||
_walletProvider = walletProvider;
|
||||
_handlers = paymentMethodHandlerDictionary;
|
||||
_policiesSettings = policiesSettings;
|
||||
_authorizationService = authorizationService;
|
||||
_appService = appService;
|
||||
_fileService = fileService;
|
||||
_networkProvider = networkProvider;
|
||||
_explorerProvider = explorerProvider;
|
||||
_btcpayServerOptions = btcpayServerOptions;
|
||||
_btcPayEnv = btcpayEnv;
|
||||
_externalServiceOptions = externalServiceOptions;
|
||||
_emailSenderFactory = emailSenderFactory;
|
||||
_onChainWalletParsers = onChainWalletParsers;
|
||||
_settingsRepository = settingsRepository;
|
||||
_eventAggregator = eventAggregator;
|
||||
_html = html;
|
||||
_dataProtector = dataProtector.CreateProtector("ConfigProtector");
|
||||
_webhookNotificationManager = webhookNotificationManager;
|
||||
_lightningNetworkOptions = lightningNetworkOptions.Value;
|
||||
}
|
||||
_rateFactory = rateFactory;
|
||||
_storeRepo = storeRepo;
|
||||
_tokenRepository = tokenRepo;
|
||||
_userManager = userManager;
|
||||
_langService = langService;
|
||||
_tokenController = tokenController;
|
||||
_walletProvider = walletProvider;
|
||||
_handlers = paymentMethodHandlerDictionary;
|
||||
_policiesSettings = policiesSettings;
|
||||
_authorizationService = authorizationService;
|
||||
_appService = appService;
|
||||
_fileService = fileService;
|
||||
_networkProvider = networkProvider;
|
||||
_explorerProvider = explorerProvider;
|
||||
_btcpayServerOptions = btcpayServerOptions;
|
||||
_btcPayEnv = btcpayEnv;
|
||||
_externalServiceOptions = externalServiceOptions;
|
||||
_emailSenderFactory = emailSenderFactory;
|
||||
_onChainWalletParsers = onChainWalletParsers;
|
||||
_settingsRepository = settingsRepository;
|
||||
_eventAggregator = eventAggregator;
|
||||
_html = html;
|
||||
_dataProtector = dataProtector.CreateProtector("ConfigProtector");
|
||||
_webhookNotificationManager = webhookNotificationManager;
|
||||
_lightningNetworkOptions = lightningNetworkOptions.Value;
|
||||
}
|
||||
|
||||
private readonly BTCPayServerOptions _btcpayServerOptions;
|
||||
private readonly BTCPayServerEnvironment _btcPayEnv;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
private readonly BitpayAccessTokenController _tokenController;
|
||||
private readonly StoreRepository _storeRepo;
|
||||
private readonly TokenRepository _tokenRepository;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly RateFetcher _rateFactory;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly ExplorerClientProvider _explorerProvider;
|
||||
private readonly LanguageService _langService;
|
||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||
private readonly PoliciesSettings _policiesSettings;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly AppService _appService;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
|
||||
private readonly EmailSenderFactory _emailSenderFactory;
|
||||
private readonly WalletFileParsers _onChainWalletParsers;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly IHtmlHelper _html;
|
||||
private readonly WebhookSender _webhookNotificationManager;
|
||||
private readonly LightningNetworkOptions _lightningNetworkOptions;
|
||||
private readonly IDataProtector _dataProtector;
|
||||
private readonly BTCPayServerOptions _btcpayServerOptions;
|
||||
private readonly BTCPayServerEnvironment _btcPayEnv;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
private readonly BitpayAccessTokenController _tokenController;
|
||||
private readonly StoreRepository _storeRepo;
|
||||
private readonly TokenRepository _tokenRepository;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly RateFetcher _rateFactory;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly ExplorerClientProvider _explorerProvider;
|
||||
private readonly LanguageService _langService;
|
||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||
private readonly PoliciesSettings _policiesSettings;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly AppService _appService;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
|
||||
private readonly EmailSenderFactory _emailSenderFactory;
|
||||
private readonly WalletFileParsers _onChainWalletParsers;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly IHtmlHelper _html;
|
||||
private readonly WebhookSender _webhookNotificationManager;
|
||||
private readonly LightningNetworkOptions _lightningNetworkOptions;
|
||||
private readonly IDataProtector _dataProtector;
|
||||
|
||||
public string? GeneratedPairingCode { get; set; }
|
||||
public string? GeneratedPairingCode { get; set; }
|
||||
|
||||
[TempData]
|
||||
private bool StoreNotConfigured { get; set; }
|
||||
[TempData]
|
||||
private bool StoreNotConfigured { get; set; }
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("{storeId}/index")]
|
||||
public async Task<IActionResult> Index(string storeId)
|
||||
{
|
||||
var userId = _userManager.GetUserId(User);
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
return Forbid();
|
||||
|
||||
var store = await _storeRepo.FindStore(storeId);
|
||||
if (store is null)
|
||||
return NotFound();
|
||||
|
||||
if ((await _authorizationService.AuthorizeAsync(User, Policies.CanModifyStoreSettings)).Succeeded)
|
||||
{
|
||||
return RedirectToAction("Dashboard", new { storeId });
|
||||
}
|
||||
if ((await _authorizationService.AuthorizeAsync(User, Policies.CanViewInvoices)).Succeeded)
|
||||
{
|
||||
return RedirectToAction("ListInvoices", "UIInvoice", new { storeId });
|
||||
}
|
||||
[AllowAnonymous]
|
||||
[HttpGet("{storeId}/index")]
|
||||
public async Task<IActionResult> Index(string storeId)
|
||||
{
|
||||
var userId = _userManager.GetUserId(User);
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
return Forbid();
|
||||
|
||||
var store = await _storeRepo.FindStore(storeId);
|
||||
if (store is null)
|
||||
return NotFound();
|
||||
|
||||
if ((await _authorizationService.AuthorizeAsync(User, Policies.CanModifyStoreSettings)).Succeeded)
|
||||
{
|
||||
return RedirectToAction("Dashboard", new { storeId });
|
||||
}
|
||||
if ((await _authorizationService.AuthorizeAsync(User, Policies.CanViewInvoices)).Succeeded)
|
||||
{
|
||||
return RedirectToAction("ListInvoices", "UIInvoice", new { storeId });
|
||||
}
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
public StoreData CurrentStore => HttpContext.GetStoreData();
|
||||
public StoreData CurrentStore => HttpContext.GetStoreData();
|
||||
|
||||
public PaymentMethodOptionViewModel.Format[] GetEnabledPaymentMethodChoices(StoreData storeData)
|
||||
{
|
||||
var enabled = storeData.GetEnabledPaymentIds();
|
||||
public PaymentMethodOptionViewModel.Format[] GetEnabledPaymentMethodChoices(StoreData storeData)
|
||||
{
|
||||
var enabled = storeData.GetEnabledPaymentIds();
|
||||
|
||||
return enabled
|
||||
.Select(o =>
|
||||
new PaymentMethodOptionViewModel.Format()
|
||||
{
|
||||
Name = o.ToString(),
|
||||
Value = o.ToString(),
|
||||
PaymentId = o
|
||||
}).ToArray();
|
||||
}
|
||||
return enabled
|
||||
.Select(o =>
|
||||
new PaymentMethodOptionViewModel.Format()
|
||||
{
|
||||
Name = o.ToString(),
|
||||
Value = o.ToString(),
|
||||
PaymentId = o
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
private string? GetUserId()
|
||||
{
|
||||
return User.Identity?.AuthenticationType != AuthenticationSchemes.Cookie ? null : _userManager.GetUserId(User);
|
||||
}
|
||||
private string? GetUserId()
|
||||
{
|
||||
return User.Identity?.AuthenticationType != AuthenticationSchemes.Cookie ? null : _userManager.GetUserId(User);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue