Convert to file-scoped namespace

This commit is contained in:
Dennis Reimann 2024-04-04 11:00:18 +02:00
parent c7a8523b77
commit 620ebc751c
No known key found for this signature in database
GPG key ID: 5009E1797F03F8D0
11 changed files with 2590 additions and 2601 deletions

View file

@ -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
});
}
}
}
}

View file

@ -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;
}
}

View file

@ -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");
}
}

View file

@ -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

View file

@ -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);
}
}

View file

@ -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 });
}
}

View file

@ -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));
}
}

View file

@ -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
});
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}