Merge branch 'master' into mwb-integration-email-qr

This commit is contained in:
d11n 2024-12-04 11:53:10 +01:00 committed by GitHub
commit 77f9b6a88c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 752 additions and 573 deletions

View file

@ -2,7 +2,7 @@ version: 2
jobs:
fast_tests:
machine:
image: ubuntu-2004:202111-02
image: ubuntu-2004:2024.11.1
steps:
- checkout
- run:
@ -10,7 +10,7 @@ jobs:
cd .circleci && ./run-tests.sh "Fast=Fast|ThirdParty=ThirdParty" && ./can-build.sh
selenium_tests:
machine:
image: ubuntu-2004:202111-02
image: ubuntu-2004:2024.11.1
steps:
- checkout
- run:
@ -18,7 +18,7 @@ jobs:
cd .circleci && ./run-tests.sh "Selenium=Selenium"
integration_tests:
machine:
image: ubuntu-2004:202111-02
image: ubuntu-2004:2024.11.1
steps:
- checkout
- run:
@ -26,7 +26,7 @@ jobs:
cd .circleci && ./run-tests.sh "Integration=Integration"
trigger_docs_build:
machine:
image: ubuntu-2004:202111-02
image: ubuntu-2004:2024.11.1
steps:
- run:
command: |

View file

@ -2,8 +2,8 @@
set -e
cd ../BTCPayServer.Tests
docker-compose -v
docker-compose -f "docker-compose.altcoins.yml" down --v
docker-compose --version
docker-compose -f "docker-compose.altcoins.yml" down -v
# For some reason, docker-compose pull fails time to time, so we try several times
n=0

View file

@ -32,8 +32,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlSanitizer" Version="8.0.838" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Client\BTCPayServer.Client.csproj" />

View file

@ -30,8 +30,8 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.5.1" />
<PackageReference Include="NBitcoin" Version="7.0.45" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.5.2" />
<PackageReference Include="NBitcoin" Version="7.0.46" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>

View file

@ -29,4 +29,10 @@ public partial class BTCPayServerClient
if (request == null) throw new ArgumentNullException(nameof(request));
await SendHttpRequest<StoreUserData>($"api/v1/stores/{storeId}/users", request, HttpMethod.Post, token);
}
public virtual async Task UpdateStoreUser(string storeId, string userId, StoreUserData request, CancellationToken token = default)
{
if (request == null) throw new ArgumentNullException(nameof(request));
await SendHttpRequest<StoreUserData>($"api/v1/stores/{storeId}/users/{userId}", request, HttpMethod.Put, token);
}
}

View file

@ -17,7 +17,25 @@ namespace BTCPayServer.Client.Models
/// </summary>
public string UserId { get; set; }
/// <summary>
/// the store role of the user
/// </summary>
public string Role { get; set; }
/// <summary>
/// the email AND username of the user
/// </summary>
public string Email { get; set; }
/// <summary>
/// the name of the user
/// </summary>
public string Name { get; set; }
/// <summary>
/// the image url of the user
/// </summary>
public string ImageUrl { get; set; }
}
public class RoleData

View file

@ -2,7 +2,7 @@
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
<ItemGroup>
<PackageReference Include="NBXplorer.Client" Version="4.3.4" />
<PackageReference Include="NBXplorer.Client" Version="4.3.6" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
</ItemGroup>
</Project>

View file

@ -1,97 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer
{
public class CustomThreadPool : IDisposable
{
readonly CancellationTokenSource _Cancel = new CancellationTokenSource();
readonly TaskCompletionSource<bool> _Exited;
int _ExitedCount = 0;
readonly Thread[] _Threads;
Exception _UnhandledException;
readonly BlockingCollection<(Action, TaskCompletionSource<object>)> _Actions = new BlockingCollection<(Action, TaskCompletionSource<object>)>(new ConcurrentQueue<(Action, TaskCompletionSource<object>)>());
public CustomThreadPool(int threadCount, string threadName)
{
if (threadCount <= 0)
throw new ArgumentOutOfRangeException(nameof(threadCount));
_Exited = new TaskCompletionSource<bool>();
_Threads = Enumerable.Range(0, threadCount).Select(_ => new Thread(RunLoop) { Name = threadName }).ToArray();
foreach (var t in _Threads)
t.Start();
}
public void Do(Action act)
{
DoAsync(act).GetAwaiter().GetResult();
}
public T Do<T>(Func<T> act)
{
return DoAsync(act).GetAwaiter().GetResult();
}
public async Task<T> DoAsync<T>(Func<T> act)
{
TaskCompletionSource<object> done = new TaskCompletionSource<object>();
_Actions.Add((() =>
{
try
{
done.TrySetResult(act());
}
catch (Exception ex) { done.TrySetException(ex); }
}
, done));
return (T)(await done.Task.ConfigureAwait(false));
}
public Task DoAsync(Action act)
{
return DoAsync<object>(() =>
{
act();
return null;
});
}
void RunLoop()
{
try
{
foreach (var act in _Actions.GetConsumingEnumerable(_Cancel.Token))
{
act.Item1();
}
}
catch (OperationCanceledException) when (_Cancel.IsCancellationRequested) { }
catch (Exception ex)
{
_Cancel.Cancel();
_UnhandledException = ex;
}
if (Interlocked.Increment(ref _ExitedCount) == _Threads.Length)
{
foreach (var action in _Actions)
{
try
{
action.Item2.TrySetCanceled();
}
catch { }
}
_Exited.TrySetResult(true);
}
}
public void Dispose()
{
_Cancel.Cancel();
_Exited.Task.GetAwaiter().GetResult();
}
}
}

View file

@ -3,11 +3,11 @@
<Import Project="../Build/Common.csproj" />
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="NBitcoin.Altcoins" Version="3.0.31" />
</ItemGroup>
<ItemGroup>

View file

@ -6,9 +6,10 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
<PackageReference Include="NBitcoin" Version="7.0.45" />
<PackageReference Include="NBitcoin" Version="7.0.46" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.4" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.2.0" />
</ItemGroup>
<ItemGroup>

View file

@ -0,0 +1,39 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class BareBitcoinRateProvider : IRateProvider
{
private readonly HttpClient _httpClient;
public RateSourceInfo RateSourceInfo => new("barebitcoin", "Bare Bitcoin", "https://api.bb.no/price");
public BareBitcoinRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
using var response = await _httpClient.GetAsync(RateSourceInfo.Url, cancellationToken);
response.EnsureSuccessStatusCode();
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
// Extract market and otc prices
var market = jobj["market"].Value<decimal>();
var buy = jobj["buy"].Value<decimal>();
var sell = jobj["sell"].Value<decimal>();
// Create currency pair for BTC/NOK
var pair = new CurrencyPair("BTC", "NOK");
// Return single pair rate with sell/buy as bid/ask
return new[] { new PairRate(pair, new BidAsk(sell, buy)) };
}
}
}

View file

@ -0,0 +1,39 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class BitmyntRateProvider : IRateProvider
{
private readonly HttpClient _httpClient;
public RateSourceInfo RateSourceInfo => new("bitmynt", "Bitmynt", "https://ny.bitmynt.no/data/rates.json");
public BitmyntRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
using var response = await _httpClient.GetAsync(RateSourceInfo.Url, cancellationToken);
response.EnsureSuccessStatusCode();
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
// Extract bid and ask prices from current_rate object
var currentRate = jobj["current_rate"];
var bid = currentRate["bid"].Value<decimal>();
var ask = currentRate["ask"].Value<decimal>();
// Create currency pair for BTC/NOK
var pair = new CurrencyPair("BTC", "NOK");
// Return single pair rate with bid/ask
return new[] { new PairRate(pair, new BidAsk(bid, ask)) };
}
}
}

View file

@ -93,16 +93,16 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewResult>(response);
// Get enabled state from settings
response = controller.WalletSettings(user.StoreId, cryptoCode).GetAwaiter().GetResult();
response = await controller.WalletSettings(user.StoreId, cryptoCode);
var onchainSettingsModel = (WalletSettingsViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.NotNull(onchainSettingsModel?.DerivationScheme);
Assert.True(onchainSettingsModel.Enabled);
// Disable wallet
onchainSettingsModel.Enabled = false;
response = controller.UpdateWalletSettings(onchainSettingsModel).GetAwaiter().GetResult();
response = await controller.UpdateWalletSettings(onchainSettingsModel);
Assert.IsType<RedirectToActionResult>(response);
response = controller.WalletSettings(user.StoreId, cryptoCode).GetAwaiter().GetResult();
response = await controller.WalletSettings(user.StoreId, cryptoCode);
onchainSettingsModel = (WalletSettingsViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.NotNull(onchainSettingsModel?.DerivationScheme);
Assert.False(onchainSettingsModel.Enabled);
@ -124,7 +124,7 @@ namespace BTCPayServer.Tests
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
// Removing the derivation scheme, should redirect to store page
response = controller.ConfirmDeleteWallet(user.StoreId, cryptoCode).GetAwaiter().GetResult();
response = await controller.ConfirmDeleteWallet(user.StoreId, cryptoCode);
Assert.IsType<RedirectToActionResult>(response);
// Setting it again should show the confirmation page
@ -174,7 +174,7 @@ namespace BTCPayServer.Tests
Assert.Equal("ElectrumFile", settingsVm.Source);
// Now let's check that no data has been lost in the process
var store = tester.PayTester.StoreRepository.FindStore(storeId).GetAwaiter().GetResult();
var store = await tester.PayTester.StoreRepository.FindStore(storeId);
var handlers = tester.PayTester.GetService<PaymentMethodHandlerDictionary>();
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
var onchainBTC = store.GetPaymentMethodConfig<DerivationSchemeSettings>(pmi, handlers);
@ -206,7 +206,7 @@ namespace BTCPayServer.Tests
Assert.Equal("paid", invoice.Status);
});
var wallet = tester.PayTester.GetController<UIWalletsController>();
var psbt = wallet.CreatePSBT(btcNetwork, onchainBTC,
var psbt = await wallet.CreatePSBT(btcNetwork, onchainBTC,
new WalletSendModel()
{
Outputs = new List<WalletSendModel.TransactionOutput>
@ -219,7 +219,7 @@ namespace BTCPayServer.Tests
}
},
FeeSatoshiPerByte = 1
}, default).GetAwaiter().GetResult();
}, default);
Assert.NotNull(psbt);
@ -440,136 +440,135 @@ namespace BTCPayServer.Tests
[Trait("Altcoins", "Altcoins")]
public async Task CanPayWithTwoCurrencies()
{
using (var tester = CreateServerTester())
using var tester = CreateServerTester();
tester.ActivateLTC();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
// First we try payment with a merchant having only BTC
var invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
{
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
var cashCow = tester.ExplorerNode;
await cashCow.GenerateAsync(2); // get some money in case
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
var firstPayment = Money.Coins(0.04m);
await cashCow.SendToAddressAsync(invoiceAddress, firstPayment);
TestUtils.Eventually(() =>
{
tester.ActivateLTC();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
// First we try payment with a merchant having only BTC
var invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
{
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.True(invoice.BtcPaid == firstPayment);
});
var cashCow = tester.ExplorerNode;
await cashCow.GenerateAsync(2); // get some money in case
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
var firstPayment = Money.Coins(0.04m);
await cashCow.SendToAddressAsync(invoiceAddress, firstPayment);
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.True(invoice.BtcPaid == firstPayment);
});
Assert.Single(invoice.CryptoInfo); // Only BTC should be presented
Assert.Single(invoice.CryptoInfo); // Only BTC should be presented
var controller = tester.PayTester.GetController<UIInvoiceController>(null);
var checkout =
(Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id, null)
.GetAwaiter().GetResult()).Value;
Assert.Single(checkout.AvailablePaymentMethods);
Assert.Equal("BTC", checkout.PaymentMethodCurrency);
Assert.Single(invoice.PaymentCodes);
Assert.Single(invoice.SupportedTransactionCurrencies);
Assert.Single(invoice.SupportedTransactionCurrencies);
Assert.Single(invoice.PaymentSubtotals);
Assert.Single(invoice.PaymentTotals);
Assert.True(invoice.PaymentCodes.ContainsKey("BTC"));
Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("BTC"));
Assert.True(invoice.SupportedTransactionCurrencies["BTC"].Enabled);
Assert.True(invoice.PaymentSubtotals.ContainsKey("BTC"));
Assert.True(invoice.PaymentTotals.ContainsKey("BTC"));
//////////////////////
// Retry now with LTC enabled
user.RegisterDerivationScheme("LTC");
invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
{
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
cashCow = tester.ExplorerNode;
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
firstPayment = Money.Coins(0.04m);
await cashCow.SendToAddressAsync(invoiceAddress, firstPayment);
TestLogs.LogInformation("First payment sent to " + invoiceAddress);
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.True(invoice.BtcPaid == firstPayment);
});
cashCow = tester.LTCExplorerNode;
var ltcCryptoInfo = invoice.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "LTC");
Assert.NotNull(ltcCryptoInfo);
invoiceAddress = BitcoinAddress.Create(ltcCryptoInfo.Address, cashCow.Network);
var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due, CultureInfo.InvariantCulture));
await cashCow.GenerateAsync(4); // LTC is not worth a lot, so just to make sure we have money...
await cashCow.SendToAddressAsync(invoiceAddress, secondPayment);
TestLogs.LogInformation("Second payment sent to " + invoiceAddress);
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(Money.Zero, invoice.BtcDue);
var ltcPaid = invoice.CryptoInfo.First(c => c.CryptoCode == "LTC");
Assert.Equal(Money.Zero, ltcPaid.Due);
Assert.Equal(secondPayment, ltcPaid.CryptoPaid);
Assert.Equal("paid", invoice.Status);
Assert.False((bool)((JValue)invoice.ExceptionStatus).Value);
});
controller = tester.PayTester.GetController<UIInvoiceController>(null);
checkout = (Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC")
var controller = tester.PayTester.GetController<UIInvoiceController>(null);
var checkout =
(Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id, null)
.GetAwaiter().GetResult()).Value;
Assert.Equal(2, checkout.AvailablePaymentMethods.Count);
Assert.Equal("LTC", checkout.PaymentMethodCurrency);
Assert.Single(checkout.AvailablePaymentMethods);
Assert.Equal("BTC", checkout.PaymentMethodCurrency);
Assert.Equal(2, invoice.PaymentCodes.Count());
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
Assert.Equal(2, invoice.PaymentSubtotals.Count());
Assert.Equal(2, invoice.PaymentTotals.Count());
Assert.True(invoice.PaymentCodes.ContainsKey("LTC"));
Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("LTC"));
Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled);
Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC"));
Assert.True(invoice.PaymentTotals.ContainsKey("LTC"));
Assert.Single(invoice.PaymentCodes);
Assert.Single(invoice.SupportedTransactionCurrencies);
Assert.Single(invoice.SupportedTransactionCurrencies);
Assert.Single(invoice.PaymentSubtotals);
Assert.Single(invoice.PaymentTotals);
Assert.True(invoice.PaymentCodes.ContainsKey("BTC"));
Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("BTC"));
Assert.True(invoice.SupportedTransactionCurrencies["BTC"].Enabled);
Assert.True(invoice.PaymentSubtotals.ContainsKey("BTC"));
Assert.True(invoice.PaymentTotals.ContainsKey("BTC"));
//////////////////////
// Check if we can disable LTC
invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
// Retry now with LTC enabled
user.RegisterDerivationScheme("LTC");
invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
{
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
cashCow = tester.ExplorerNode;
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
firstPayment = Money.Coins(0.04m);
await cashCow.SendToAddressAsync(invoiceAddress, firstPayment);
TestLogs.LogInformation("First payment sent to " + invoiceAddress);
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.True(invoice.BtcPaid == firstPayment);
});
cashCow = tester.LTCExplorerNode;
var ltcCryptoInfo = invoice.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "LTC");
Assert.NotNull(ltcCryptoInfo);
invoiceAddress = BitcoinAddress.Create(ltcCryptoInfo.Address, cashCow.Network);
var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due, CultureInfo.InvariantCulture));
await cashCow.GenerateAsync(4); // LTC is not worth a lot, so just to make sure we have money...
await cashCow.SendToAddressAsync(invoiceAddress, secondPayment);
TestLogs.LogInformation("Second payment sent to " + invoiceAddress);
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(Money.Zero, invoice.BtcDue);
var ltcPaid = invoice.CryptoInfo.First(c => c.CryptoCode == "LTC");
Assert.Equal(Money.Zero, ltcPaid.Due);
Assert.Equal(secondPayment, ltcPaid.CryptoPaid);
Assert.Equal("paid", invoice.Status);
Assert.False((bool)((JValue)invoice.ExceptionStatus).Value);
});
controller = tester.PayTester.GetController<UIInvoiceController>(null);
checkout = (Models.InvoicingModels.CheckoutModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC")
.GetAwaiter().GetResult()).Value;
Assert.Equal(2, checkout.AvailablePaymentMethods.Count);
Assert.Equal("LTC", checkout.PaymentMethodCurrency);
Assert.Equal(2, invoice.PaymentCodes.Count());
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
Assert.Equal(2, invoice.PaymentSubtotals.Count());
Assert.Equal(2, invoice.PaymentTotals.Count());
Assert.True(invoice.PaymentCodes.ContainsKey("LTC"));
Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("LTC"));
Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled);
Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC"));
Assert.True(invoice.PaymentTotals.ContainsKey("LTC"));
// Check if we can disable LTC
invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
{
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true,
SupportedTransactionCurrencies = new Dictionary<string, InvoiceSupportedTransactionCurrency>()
{
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true,
SupportedTransactionCurrencies = new Dictionary<string, InvoiceSupportedTransactionCurrency>()
{
{"BTC", new InvoiceSupportedTransactionCurrency() {Enabled = true}}
}
}, Facade.Merchant);
{"BTC", new InvoiceSupportedTransactionCurrency() {Enabled = true}}
}
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo.Where(c => c.CryptoCode == "BTC"));
Assert.Empty(invoice.CryptoInfo.Where(c => c.CryptoCode == "LTC"));
}
Assert.Single(invoice.CryptoInfo, c => c.CryptoCode == "BTC");
Assert.DoesNotContain(invoice.CryptoInfo, c => c.CryptoCode == "LTC");
}
[Fact]
@ -744,7 +743,7 @@ noninventoryitem:
invoices = user.BitPay.GetInvoices();
Assert.Equal(2, invoices.Count(invoice => invoice.ItemCode.Equals("noninventoryitem")));
var inventoryItemInvoice =
Assert.Single(invoices.Where(invoice => invoice.ItemCode.Equals("inventoryitem")));
Assert.Single(invoices, invoice => invoice.ItemCode.Equals("inventoryitem"));
Assert.NotNull(inventoryItemInvoice);
//let's mark the inventoryitem invoice as invalid, this should return the item to back in stock

View file

@ -39,13 +39,14 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.15" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.16" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.11" />
<PackageReference Include="Selenium.Support" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver" Version="4.22.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="128.0.6613.11900" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

View file

@ -263,7 +263,7 @@ namespace BTCPayServer.Tests
});
TestLogs.LogInformation("Because UseAllStoreInvoices is true, let's make sure the invoice is tagged");
var invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult();
var invoiceEntity = await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id);
Assert.True(invoiceEntity.Version >= InvoiceEntity.InternalTagSupport_Version);
Assert.Contains(AppService.GetAppInternalTag(app.Id), invoiceEntity.InternalTags);
@ -281,7 +281,7 @@ namespace BTCPayServer.Tests
TransactionSpeed = "high",
FullNotifications = true
}, Facade.Merchant);
invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult();
invoiceEntity = await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id);
Assert.DoesNotContain(AppService.GetAppInternalTag(app.Id), invoiceEntity.InternalTags);
TestLogs.LogInformation("After turning setting a softcap, let's check that only actual payments are counted");

View file

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0.101-bookworm-slim AS builder
FROM mcr.microsoft.com/dotnet/sdk:8.0.404-bookworm-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends chromium-driver \
&& rm -rf /var/lib/apt/lists/*

View file

@ -174,10 +174,10 @@ namespace BTCPayServer.Tests
public void CanRandomizeByPercentage()
{
var generated = Enumerable.Range(0, 1000).Select(_ => MempoolSpaceFeeProvider.RandomizeByPercentage(100.0m, 10.0m)).ToArray();
Assert.Empty(generated.Where(g => g < 90m));
Assert.Empty(generated.Where(g => g > 110m));
Assert.NotEmpty(generated.Where(g => g < 91m));
Assert.NotEmpty(generated.Where(g => g > 109m));
Assert.DoesNotContain(generated, g => g < 90m);
Assert.DoesNotContain(generated, g => g > 110m);
Assert.Contains(generated, g => g < 91m);
Assert.Contains(generated, g => g > 109m);
}
private void CanParseDecimalsCore(string str, decimal expected)
@ -793,9 +793,9 @@ namespace BTCPayServer.Tests
}), BTCPayLogs);
await tor.Refresh();
Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.BTCPayServer));
Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.P2P));
Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.RPC));
Assert.Single(tor.Services, t => t.ServiceType == TorServiceType.BTCPayServer);
Assert.Single(tor.Services, t => t.ServiceType == TorServiceType.P2P);
Assert.Single(tor.Services, t => t.ServiceType == TorServiceType.RPC);
Assert.True(tor.Services.Count(t => t.ServiceType == TorServiceType.Other) > 1);
tor = new TorServices(CreateNetworkProvider(ChainName.Regtest),
@ -806,24 +806,24 @@ namespace BTCPayServer.Tests
}), BTCPayLogs);
await Task.WhenAll(tor.StartAsync(CancellationToken.None));
var btcpayS = Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.BTCPayServer));
var btcpayS = Assert.Single(tor.Services, t => t.ServiceType == TorServiceType.BTCPayServer);
Assert.Null(btcpayS.Network);
Assert.Equal("host.onion", btcpayS.OnionHost);
Assert.Equal(80, btcpayS.VirtualPort);
var p2p = Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.P2P));
var p2p = Assert.Single(tor.Services, t => t.ServiceType == TorServiceType.P2P);
Assert.NotNull(p2p.Network);
Assert.Equal("BTC", p2p.Network.CryptoCode);
Assert.Equal("host2.onion", p2p.OnionHost);
Assert.Equal(81, p2p.VirtualPort);
var rpc = Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.RPC));
var rpc = Assert.Single(tor.Services, t => t.ServiceType == TorServiceType.RPC);
Assert.NotNull(p2p.Network);
Assert.Equal("BTC", rpc.Network.CryptoCode);
Assert.Equal("host3.onion", rpc.OnionHost);
Assert.Equal(82, rpc.VirtualPort);
var unknown = Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.Other));
var unknown = Assert.Single(tor.Services, t => t.ServiceType == TorServiceType.Other);
Assert.Null(unknown.Network);
Assert.Equal("host4.onion", unknown.OnionHost);
Assert.Equal(83, unknown.VirtualPort);

View file

@ -3867,7 +3867,7 @@ namespace BTCPayServer.Tests
void VerifyLightning(GenericPaymentMethodData[] methods)
{
var m = Assert.Single(methods.Where(m => m.PaymentMethodId == "BTC-LN"));
var m = Assert.Single(methods, m => m.PaymentMethodId == "BTC-LN");
Assert.Equal("Internal Node", m.Config["internalNodeRef"].Value<string>());
}
@ -3879,7 +3879,7 @@ namespace BTCPayServer.Tests
void VerifyOnChain(GenericPaymentMethodData[] dictionary)
{
var m = Assert.Single(methods.Where(m => m.PaymentMethodId == "BTC-CHAIN"));
var m = Assert.Single(methods, m => m.PaymentMethodId == "BTC-CHAIN");
var paymentMethodBaseData = Assert.IsType<JObject>(m.Config);
Assert.Equal(wallet.Config.AccountDerivation, paymentMethodBaseData["accountDerivation"].Value<string>());
}
@ -3985,7 +3985,12 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
await user.GrantAccessAsync(true);
var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings);
var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings, Policies.CanModifyProfile);
await client.UpdateCurrentUser(new UpdateApplicationUserRequest
{
Name = "The Admin",
ImageUrl = "avatar.jpg"
});
var roles = await client.GetServerRoles();
Assert.Equal(4, roles.Count);
@ -3999,6 +4004,9 @@ namespace BTCPayServer.Tests
var storeUser = Assert.Single(users);
Assert.Equal(user.UserId, storeUser.UserId);
Assert.Equal(ownerRole.Id, storeUser.Role);
Assert.Equal(user.Email, storeUser.Email);
Assert.Equal("The Admin", storeUser.Name);
Assert.Equal("avatar.jpg", storeUser.ImageUrl);
var manager = tester.NewAccount();
await manager.GrantAccessAsync();
var employee = tester.NewAccount();
@ -4029,7 +4037,14 @@ namespace BTCPayServer.Tests
// add users to store
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = managerRole.Id, UserId = manager.UserId });
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = employeeRole.Id, UserId = employee.UserId });
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = guestRole.Id, UserId = guest.UserId });
// add with email
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = guestRole.Id, UserId = guest.Email });
// test unknown user
await AssertAPIError("user-not-found", async () => await client.AddStoreUser(user.StoreId, new StoreUserData { Role = managerRole.Id, UserId = "unknown" }));
await AssertAPIError("user-not-found", async () => await client.UpdateStoreUser(user.StoreId, "unknown", new StoreUserData { Role = ownerRole.Id }));
await AssertAPIError("user-not-found", async () => await client.RemoveStoreUser(user.StoreId, "unknown"));
//test no access to api for employee
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await employeeClient.GetStore(user.StoreId));
@ -4050,9 +4065,14 @@ namespace BTCPayServer.Tests
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await managerClient.RemoveStoreUser(user.StoreId, user.UserId));
// updates
await client.UpdateStoreUser(user.StoreId, employee.UserId, new StoreUserData { Role = ownerRole.Id });
await employeeClient.GetStore(user.StoreId);
// remove
await client.RemoveStoreUser(user.StoreId, employee.UserId);
await AssertHttpError(403, async () => await employeeClient.GetStore(user.StoreId));
// test duplicate add
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = ownerRole.Id, UserId = employee.UserId });
await AssertAPIError("duplicate-store-user-role", async () =>
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = ownerRole.Id, UserId = employee.UserId }));
@ -4412,8 +4432,8 @@ namespace BTCPayServer.Tests
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Equal(3, payouts.Length);
Assert.Empty(payouts.Where(data => data.State == PayoutState.AwaitingApproval));
Assert.Empty(payouts.Where(data => data.PayoutAmount is null));
Assert.DoesNotContain(payouts, data => data.State == PayoutState.AwaitingApproval);
Assert.DoesNotContain(payouts, data => data.PayoutAmount is null);
Assert.Empty(await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC"));
@ -4456,7 +4476,7 @@ namespace BTCPayServer.Tests
{
Assert.Equal(2, (await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")).Count());
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress));
Assert.Single(payouts, data => data.State == PayoutState.InProgress);
});
uint256 txid = null;
@ -4470,7 +4490,7 @@ namespace BTCPayServer.Tests
{
Assert.Equal(4, (await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")).Count());
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));
Assert.DoesNotContain(payouts, data => data.State != PayoutState.InProgress);
});
// settings that were added later
@ -4536,7 +4556,7 @@ namespace BTCPayServer.Tests
payouts = await adminClient.GetStorePayouts(admin.StoreId);
try
{
Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress && data.Id == payoutThatShouldBeProcessedStraightAway.Id));
Assert.Single(payouts, data => data.State == PayoutState.InProgress && data.Id == payoutThatShouldBeProcessedStraightAway.Id);
}
catch (SingleException)
{
@ -4580,7 +4600,7 @@ namespace BTCPayServer.Tests
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Single(payouts.Where(data => data.State == PayoutState.AwaitingPayment && data.Id == payoutThatShouldNotBeProcessedStraightAway.Id));
Assert.Single(payouts, data => data.State == PayoutState.AwaitingPayment && data.Id == payoutThatShouldNotBeProcessedStraightAway.Id);
beforeHookTcs = new TaskCompletionSource();
afterHookTcs = new TaskCompletionSource();
@ -4613,7 +4633,7 @@ namespace BTCPayServer.Tests
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));
Assert.DoesNotContain(payouts, data => data.State != PayoutState.InProgress);
}
@ -4690,11 +4710,11 @@ namespace BTCPayServer.Tests
await client.AddOrUpdateOnChainWalletObject(admin.StoreId, "BTC", new AddOnChainWalletObjectRequest() { Type = "newtype", Id = test1.Id });
testObj = await client.GetOnChainWalletObject(admin.StoreId, "BTC", test);
Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.LinkData["testData"]?.Value<string>() == "lol"));
Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.ObjectData["testData"]?.Value<string>() == "test1"));
Assert.Single(testObj.Links, l => l.Id == "test1" && l.LinkData["testData"]?.Value<string>() == "lol");
Assert.Single(testObj.Links, l => l.Id == "test1" && l.ObjectData["testData"]?.Value<string>() == "test1");
testObj = await client.GetOnChainWalletObject(admin.StoreId, "BTC", test, false);
Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.LinkData["testData"]?.Value<string>() == "lol"));
Assert.Single(testObj.Links.Where(l => l.Id == "test1" && l.ObjectData is null));
Assert.Single(testObj.Links, l => l.Id == "test1" && l.LinkData["testData"]?.Value<string>() == "lol");
Assert.Single(testObj.Links, l => l.Id == "test1" && l.ObjectData is null);
async Task TestWalletRepository()
{

View file

@ -385,10 +385,6 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("123456");
s.ClickPagePrimary();
Assert.Contains("Account successfully created.", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("Email")).SendKeys(usr);
s.Driver.FindElement(By.Id("Password")).SendKeys("123456");
s.Driver.FindElement(By.Id("LoginButton")).Click();
// We should be logged in now
s.GoToHome();

View file

@ -208,6 +208,18 @@ namespace BTCPayServer.Tests
e => e.CurrencyPair == new CurrencyPair("BTC", "LBP") &&
e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 LBP (I hope)
}
else if (name == "bitmynt")
{
Assert.Contains(exchangeRates.ByExchange[name],
e => e.CurrencyPair == new CurrencyPair("BTC", "NOK") &&
e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 NOK
}
else if (name == "barebitcoin")
{
Assert.Contains(exchangeRates.ByExchange[name],
e => e.CurrencyPair == new CurrencyPair("BTC", "NOK") &&
e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 NOK
}
else
{
if (name == "kraken")
@ -234,7 +246,7 @@ namespace BTCPayServer.Tests
}
// Kraken emit one request only after first GetRates
factory.Providers["kraken"].GetRatesAsync(default).GetAwaiter().GetResult();
await factory.Providers["kraken"].GetRatesAsync(default);
var p = new KrakenExchangeRateProvider();
var rates = await p.GetRatesAsync(default);
@ -339,7 +351,7 @@ retry:
}
[Fact]
public void CanSolveTheDogesRatesOnKraken()
public async Task CanSolveTheDogesRatesOnKraken()
{
var factory = FastTests.CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
@ -347,7 +359,7 @@ retry:
Assert.True(RateRules.TryParse("X_X=kraken(X_BTC) * kraken(BTC_X)", out var rule));
foreach (var pair in new[] { "DOGE_USD", "DOGE_CAD" })
{
var result = fetcher.FetchRate(CurrencyPair.Parse(pair), rule, null, default).GetAwaiter().GetResult();
var result = await fetcher.FetchRate(CurrencyPair.Parse(pair), rule, null, default);
Assert.NotNull(result.BidAsk);
Assert.Empty(result.Errors);
}
@ -601,7 +613,7 @@ retry:
foreach (var rate in rates)
{
Assert.Single(rates.Where(r => r == rate));
Assert.Single(rates, r => r == rate);
}
}

View file

@ -194,7 +194,7 @@ namespace BTCPayServer.Tests
[Fact]
[Trait("Integration", "Integration")]
public async void CanStoreArbitrarySettingsWithStore()
public async Task CanStoreArbitrarySettingsWithStore()
{
using var tester = CreateServerTester();
await tester.StartAsync();
@ -446,25 +446,25 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewResult>(storeResponse);
Assert.IsType<ViewResult>(storeController.SetupLightningNode(user.StoreId, "BTC"));
storeController.SetupLightningNode(user.StoreId, new LightningNodeViewModel
await storeController.SetupLightningNode(user.StoreId, new LightningNodeViewModel
{
ConnectionString = $"type=charge;server={tester.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true",
SkipPortTest = true // We can't test this as the IP can't be resolved by the test host :(
}, "test", "BTC").GetAwaiter().GetResult();
}, "test", "BTC");
Assert.False(storeController.TempData.ContainsKey(WellKnownTempData.ErrorMessage));
storeController.TempData.Clear();
Assert.True(storeController.ModelState.IsValid);
Assert.IsType<RedirectToActionResult>(storeController.SetupLightningNode(user.StoreId,
Assert.IsType<RedirectToActionResult>(await storeController.SetupLightningNode(user.StoreId,
new LightningNodeViewModel
{
ConnectionString = $"type=charge;server={tester.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true"
}, "save", "BTC").GetAwaiter().GetResult());
}, "save", "BTC"));
// Make sure old connection string format does not work
Assert.IsType<RedirectToActionResult>(storeController.SetupLightningNode(user.StoreId,
Assert.IsType<RedirectToActionResult>(await storeController.SetupLightningNode(user.StoreId,
new LightningNodeViewModel { ConnectionString = tester.MerchantCharge.Client.Uri.AbsoluteUri },
"save", "BTC").GetAwaiter().GetResult());
"save", "BTC"));
storeResponse = storeController.LightningSettings(user.StoreId, "BTC");
var storeVm =
@ -1099,7 +1099,7 @@ namespace BTCPayServer.Tests
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async void CheckCORSSetOnBitpayAPI()
public async Task CheckCORSSetOnBitpayAPI()
{
using var tester = CreateServerTester();
await tester.StartAsync();
@ -1143,9 +1143,8 @@ namespace BTCPayServer.Tests
// Test request pairing code client side
var storeController = user.GetController<UIStoresController>();
storeController
.CreateToken(user.StoreId, new CreateTokenViewModel() { Label = "test2", StoreId = user.StoreId })
.GetAwaiter().GetResult();
await storeController
.CreateToken(user.StoreId, new CreateTokenViewModel() { Label = "test2", StoreId = user.StoreId });
Assert.NotNull(storeController.GeneratedPairingCode);
@ -1169,17 +1168,17 @@ namespace BTCPayServer.Tests
// Can generate API Key
var repo = tester.PayTester.GetService<TokenRepository>();
Assert.Empty(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>()
.GenerateAPIKey(user.StoreId).GetAwaiter().GetResult());
Assert.Empty(await repo.GetLegacyAPIKeys(user.StoreId));
Assert.IsType<RedirectToActionResult>(await user.GetController<UIStoresController>()
.GenerateAPIKey(user.StoreId));
var apiKey = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
var apiKey = Assert.Single(await repo.GetLegacyAPIKeys(user.StoreId));
///////
// Generating a new one remove the previous
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>()
.GenerateAPIKey(user.StoreId).GetAwaiter().GetResult());
var apiKey2 = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
Assert.IsType<RedirectToActionResult>(await user.GetController<UIStoresController>()
.GenerateAPIKey(user.StoreId));
var apiKey2 = Assert.Single(await repo.GetLegacyAPIKeys(user.StoreId));
Assert.NotEqual(apiKey, apiKey2);
////////
@ -1193,7 +1192,7 @@ namespace BTCPayServer.Tests
var invoice = new Invoice() { Price = 5000.0m, Currency = "USD" };
message.Content = new StringContent(JsonConvert.SerializeObject(invoice), Encoding.UTF8,
"application/json");
var result = client.SendAsync(message).GetAwaiter().GetResult();
var result = await client.SendAsync(message);
result.EnsureSuccessStatusCode();
/////////////////////
@ -1207,7 +1206,7 @@ namespace BTCPayServer.Tests
mess.Headers.Add("x-identity",
"04b4d82095947262dd70f94c0a0e005ec3916e3f5f2181c176b8b22a52db22a8c436c4703f43a9e8884104854a11e1eb30df8fdf116e283807a1f1b8fe4c182b99");
mess.Method = HttpMethod.Get;
result = client.SendAsync(mess).GetAwaiter().GetResult();
result = await client.SendAsync(mess);
Assert.Equal(System.Net.HttpStatusCode.Unauthorized, result.StatusCode);
//
@ -1539,7 +1538,7 @@ namespace BTCPayServer.Tests
// We allow BTC and LN, but not BTC under 5 USD, so only LN should be in the invoice
var vm = await user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModelAsync<CheckoutAppearanceViewModel>();
Assert.Equal(2, vm.PaymentMethodCriteria.Count);
var criteria = Assert.Single(vm.PaymentMethodCriteria.Where(m => m.PaymentMethod == btcMethod.ToString()));
var criteria = Assert.Single(vm.PaymentMethodCriteria, m => m.PaymentMethod == btcMethod.ToString());
Assert.Equal(btcMethod.ToString(), criteria.PaymentMethod);
criteria.Value = "5 USD";
criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan;
@ -2175,19 +2174,19 @@ namespace BTCPayServer.Tests
Assert.Equal(0, invoice.CryptoInfo[0].TxCount);
Assert.True(invoice.MinerFees.ContainsKey("BTC"));
Assert.Contains(Math.Round(invoice.MinerFees["BTC"].SatoshiPerBytes), new[] { 100.0m, 20.0m });
TestUtils.Eventually(() =>
await TestUtils.EventuallyAsync(async () =>
{
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
var textSearchResult = await tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = new[] { user.StoreId },
TextSearch = invoice.OrderId
}).GetAwaiter().GetResult();
});
Assert.Single(textSearchResult);
textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
textSearchResult = await tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = new[] { user.StoreId },
TextSearch = invoice.Id
}).GetAwaiter().GetResult();
});
Assert.Single(textSearchResult);
});
@ -2215,11 +2214,11 @@ namespace BTCPayServer.Tests
Assert.True(IsMapped(invoice, ctx));
cashCow.SendToAddress(invoiceAddress, firstPayment);
var invoiceEntity = repo.GetInvoice(invoice.Id, true).GetAwaiter().GetResult();
var invoiceEntity = await repo.GetInvoice(invoice.Id, true);
Money secondPayment = Money.Zero;
TestUtils.Eventually(() =>
await TestUtils.EventuallyAsync(async () =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("new", localInvoice.Status);
@ -2231,7 +2230,7 @@ namespace BTCPayServer.Tests
Assert.True(IsMapped(invoice, ctx));
Assert.True(IsMapped(localInvoice, ctx));
invoiceEntity = repo.GetInvoice(invoice.Id, true).GetAwaiter().GetResult();
invoiceEntity = await repo.GetInvoice(invoice.Id, true);
invoiceAddress = BitcoinAddress.Create(localInvoice.BitcoinAddress, cashCow.Network);
secondPayment = localInvoice.BtcDue;
});
@ -2274,18 +2273,18 @@ namespace BTCPayServer.Tests
var txId = await cashCow.SendToAddressAsync(invoiceAddress, invoice.BtcDue + Money.Coins(1));
TestUtils.Eventually(() =>
await TestUtils.EventuallyAsync(async () =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
var textSearchResult = await tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = new[] { user.StoreId },
TextSearch = txId.ToString()
}).GetAwaiter().GetResult();
});
Assert.Single(textSearchResult);
});
@ -2421,7 +2420,7 @@ namespace BTCPayServer.Tests
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async void CheckOnionlocationForNonOnionHtmlRequests()
public async Task CheckOnionlocationForNonOnionHtmlRequests()
{
using var tester = CreateServerTester();
await tester.StartAsync();
@ -3048,7 +3047,7 @@ namespace BTCPayServer.Tests
[Fact]
[Trait("Integration", "Integration")]
public async void CanUseLocalProviderFiles()
public async Task CanUseLocalProviderFiles()
{
using var tester = CreateServerTester();
await tester.StartAsync();
@ -3273,7 +3272,7 @@ namespace BTCPayServer.Tests
var fullyPaidIndex = report.GetIndex("FullyPaid");
var completedIndex = report.GetIndex("Completed");
var limitIndex = report.GetIndex("Limit");
var d = Assert.Single(report.Data.Where(d => d[report.GetIndex("InvoiceId")].Value<string>() == inv.Id));
var d = Assert.Single(report.Data, d => d[report.GetIndex("InvoiceId")].Value<string>() == inv.Id);
Assert.Equal(fullyPaid, (bool)d[fullyPaidIndex]);
Assert.Equal(currency, d[currencyIndex].Value<string>());
Assert.Equal(completed, GetAmount(completedIndex, d));

View file

@ -98,7 +98,7 @@ services:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.5.12
image: nicolasdorier/nbxplorer:2.5.14
restart: unless-stopped
ports:
- "32838:32838"

View file

@ -95,7 +95,7 @@ services:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.5.12
image: nicolasdorier/nbxplorer:2.5.14
restart: unless-stopped
ports:
- "32838:32838"

View file

@ -1,6 +1,5 @@
{
"maxParallelThreads": 4,
"longRunningTestSeconds": 60,
"diagnosticMessages": true,
"methodDisplay": "method"
}

View file

@ -8,6 +8,19 @@
<RunAnalyzersDuringLiveAnalysis>False</RunAnalyzersDuringLiveAnalysis>
<RunAnalyzersDuringBuild>False</RunAnalyzersDuringBuild>
</PropertyGroup>
<!-- Pre-compiling views should only be done for Release builds without dotnet watch or design time build .-->
<!-- Runtime compiling is only useful for debugging with hot reload of the views -->
<PropertyGroup Condition="'$(RazorCompileOnBuild)'=='' AND ('$(Configuration)' == 'Debug' OR '$(DotNetWatchBuild)' == 'true' OR '$(DesignTimeBuild)' == 'true')">
<RazorCompileOnBuild>false</RazorCompileOnBuild>
</PropertyGroup>
<PropertyGroup Condition="'$(RazorCompileOnBuild)'==''">
<RazorCompileOnBuild>true</RazorCompileOnBuild>
</PropertyGroup>
<PropertyGroup Condition="'$(RazorCompileOnBuild)' == 'true'">
<DefineConstants>$(DefineConstants);RAZOR_COMPILE_ON_BUILD</DefineConstants>
</PropertyGroup>
<ItemGroup>
<AssemblyAttribute Condition="'$(GitCommit)' != ''" Include="BTCPayServer.GitCommitAttribute">
<_Parameter1>$(GitCommit)</_Parameter1>
@ -37,17 +50,17 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.24" />
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.25" />
<PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.6.7" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.6.9" />
<PackageReference Include="CsvHelper" Version="32.0.3" />
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Fido2" Version="2.0.2" />
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
<PackageReference Include="LNURL" Version="0.0.34" />
<PackageReference Include="MailKit" Version="3.3.0" />
<PackageReference Include="Fido2" Version="3.0.1" />
<PackageReference Include="Fido2.AspNet" Version="3.0.1" />
<PackageReference Include="LNURL" Version="0.0.36" />
<PackageReference Include="MailKit" Version="4.8.0" />
<PackageReference Include="BTCPayServer.NETCore.Plugins.Mvc" Version="1.4.4" />
<PackageReference Include="QRCoder" Version="1.6.0" />
<PackageReference Include="System.IO.Pipelines" Version="8.0.0" />
@ -60,13 +73,14 @@
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.1-dev-00968" />
<PackageReference Include="SSH.NET" Version="2023.0.0" />
<PackageReference Include="TwentyTwenty.Storage" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Amazon" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.7" />
<PackageReference Include="TwentyTwenty.Storage" Version="2.24.2" />
<PackageReference Include="TwentyTwenty.Storage.Amazon" Version="2.24.2" />
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.24.2" />
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.24.2" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.24.2" />
<PackageReference Condition="'$(RazorCompileOnBuild)' == 'false'" Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.11" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
</ItemGroup>
<ItemGroup>

View file

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers.Greenfield
{
@ -30,10 +31,10 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/users")]
public IActionResult GetStoreUsers()
public async Task<IActionResult> GetStoreUsers()
{
var store = HttpContext.GetStoreData();
return store == null ? StoreNotFound() : Ok(FromModel(store));
return store == null ? StoreNotFound() : Ok(await FromModel(store));
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
@ -41,31 +42,28 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> RemoveStoreUser(string storeId, string idOrEmail)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return StoreNotFound();
}
if (store == null) return StoreNotFound();
var userId = await _userManager.FindByIdOrEmail(idOrEmail);
if (userId != null && await _storeRepository.RemoveStoreUser(storeId, idOrEmail))
{
return Ok();
}
return this.CreateAPIError(409, "store-user-role-orphaned", "Removing this user would result in the store having no owner.");
var user = await _userManager.FindByIdOrEmail(idOrEmail);
if (user == null) return UserNotFound();
return await _storeRepository.RemoveStoreUser(storeId, user.Id)
? Ok()
: this.CreateAPIError(409, "store-user-role-orphaned", "Removing this user would result in the store having no owner.");
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/users")]
public async Task<IActionResult> AddStoreUser(string storeId, StoreUserData request)
[HttpPut("~/api/v1/stores/{storeId}/users/{idOrEmail?}")]
public async Task<IActionResult> AddOrUpdateStoreUser(string storeId, StoreUserData request, string idOrEmail = null)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return StoreNotFound();
}
StoreRoleId roleId = null;
if (store == null) return StoreNotFound();
var user = await _userManager.FindByIdOrEmail(idOrEmail ?? request.UserId);
if (user == null) return UserNotFound();
StoreRoleId roleId = null;
if (request.Role is not null)
{
roleId = await _storeRepository.ResolveStoreRoleId(storeId, request.Role);
@ -76,21 +74,42 @@ namespace BTCPayServer.Controllers.Greenfield
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
if (await _storeRepository.AddStoreUser(storeId, request.UserId, roleId))
{
return Ok();
}
return this.CreateAPIError(409, "duplicate-store-user-role", "The user is already added to the store");
var result = string.IsNullOrEmpty(idOrEmail)
? await _storeRepository.AddStoreUser(storeId, user.Id, roleId)
: await _storeRepository.AddOrUpdateStoreUser(storeId, user.Id, roleId);
return result
? Ok()
: this.CreateAPIError(409, "duplicate-store-user-role", "The user is already added to the store");
}
private IEnumerable<StoreUserData> FromModel(Data.StoreData data)
private async Task<IEnumerable<StoreUserData>> FromModel(StoreData data)
{
return data.UserStores.Select(store => new StoreUserData() { UserId = store.ApplicationUserId, Role = store.StoreRoleId });
var storeUsers = new List<StoreUserData>();
foreach (var storeUser in data.UserStores)
{
var user = await _userManager.FindByIdOrEmail(storeUser.ApplicationUserId);
var blob = user?.GetBlob();
storeUsers.Add(new StoreUserData
{
UserId = storeUser.ApplicationUserId,
Role = storeUser.StoreRoleId,
Email = user?.Email,
Name = blob?.Name,
ImageUrl = blob?.ImageUrl,
});
}
return storeUsers;
}
private IActionResult StoreNotFound()
{
return this.CreateAPIError(404, "store-not-found", "The store was not found");
}
private IActionResult UserNotFound()
{
return this.CreateAPIError(404, "user-not-found", "The user was not found");
}
}
}

View file

@ -985,17 +985,22 @@ namespace BTCPayServer.Controllers.Greenfield
return GetFromActionResult(await GetController<GreenfieldUsersController>().GetUsers());
}
public override Task<IEnumerable<StoreUserData>> GetStoreUsers(string storeId,
public override async Task<IEnumerable<StoreUserData>> GetStoreUsers(string storeId,
CancellationToken token = default)
{
return Task.FromResult(
GetFromActionResult<IEnumerable<StoreUserData>>(GetController<GreenfieldStoreUsersController>().GetStoreUsers()));
return GetFromActionResult<IEnumerable<StoreUserData>>(await GetController<GreenfieldStoreUsersController>().GetStoreUsers());
}
public override async Task AddStoreUser(string storeId, StoreUserData request,
CancellationToken token = default)
{
HandleActionResult(await GetController<GreenfieldStoreUsersController>().AddStoreUser(storeId, request));
HandleActionResult(await GetController<GreenfieldStoreUsersController>().AddOrUpdateStoreUser(storeId, request));
}
public override async Task UpdateStoreUser(string storeId, string userId, StoreUserData request,
CancellationToken token = default)
{
HandleActionResult(await GetController<GreenfieldStoreUsersController>().AddOrUpdateStoreUser(storeId, request, userId));
}
public override async Task RemoveStoreUser(string storeId, string userId, CancellationToken token = default)

View file

@ -273,7 +273,7 @@ namespace BTCPayServer.Controllers
}
return new LoginWithFido2ViewModel
{
Data = r,
Data = System.Text.Json.JsonSerializer.Serialize(r, r.GetType()),
UserId = user.Id,
RememberMe = rememberMe
};
@ -385,7 +385,7 @@ namespace BTCPayServer.Controllers
try
{
if (await _fido2Service.CompleteLogin(viewModel.UserId, JObject.Parse(viewModel.Response).ToObject<AuthenticatorAssertionRawResponse>()))
if (await _fido2Service.CompleteLogin(viewModel.UserId, System.Text.Json.JsonSerializer.Deserialize<AuthenticatorAssertionRawResponse>(viewModel.Response)))
{
await _signInManager.SignInAsync(user!, viewModel.RememberMe, "FIDO2");
_logger.LogInformation("User {Email} logged in with FIDO2", user.Email);
@ -650,6 +650,7 @@ namespace BTCPayServer.Controllers
if (logon)
{
await _signInManager.SignInAsync(user, isPersistent: false);
_logger.LogInformation("User {Email} logged in", user.Email);
return RedirectToLocal(returnUrl);
}
}
@ -793,7 +794,7 @@ namespace BTCPayServer.Controllers
[HttpPost("/login/set-password")]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SetPassword(SetPasswordViewModel model)
public async Task<IActionResult> SetPassword(SetPasswordViewModel model, string returnUrl = null)
{
if (!ModelState.IsValid)
{
@ -802,9 +803,11 @@ namespace BTCPayServer.Controllers
var user = await _userManager.FindByEmailAsync(model.Email);
var hasPassword = user != null && await _userManager.HasPasswordAsync(user);
if (!UserService.TryCanLogin(user, out _))
var needsInitialPassword = user != null && !await _userManager.HasPasswordAsync(user);
// Let unapproved users set a password. Otherwise, don't reveal that the user does not exist.
if (!UserService.TryCanLogin(user, out var message) && !needsInitialPassword || user == null)
{
// Don't reveal that the user does not exist
_logger.LogWarning("User {Email} tried to reset password, but failed: {Message}", user?.Email ?? "(NO EMAIL)", message);
return RedirectToAction(nameof(Login));
}
@ -818,7 +821,19 @@ namespace BTCPayServer.Controllers
? StringLocalizer["Password successfully set."].Value
: StringLocalizer["Account successfully created."].Value
});
if (!hasPassword) await FinalizeInvitationIfApplicable(user);
// see if we can sign in user after accepting an invitation and setting the password
if (needsInitialPassword && UserService.TryCanLogin(user, out _))
{
var signInResult = await _signInManager.PasswordSignInAsync(user.Email!, model.Password, true, true);
if (signInResult.Succeeded)
{
_logger.LogInformation("User {Email} logged in", user.Email);
return RedirectToLocal(returnUrl);
}
}
return RedirectToAction(nameof(Login));
}

View file

@ -33,7 +33,6 @@ using NBitcoin;
using Newtonsoft.Json.Linq;
using StoreData = BTCPayServer.Data.StoreData;
using Serilog.Filters;
using PeterO.Numbers;
using BTCPayServer.Payouts;
using Microsoft.Extensions.Localization;

View file

@ -13,7 +13,6 @@ using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
using Newtonsoft.Json.Linq;
using static Org.BouncyCastle.Math.EC.ECCurve;
namespace BTCPayServer.Data
{

View file

@ -11,6 +11,7 @@ using Fido2NetLib.Objects;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using Newtonsoft.Json.Linq;
using static BTCPayServer.Fido2.Models.Fido2CredentialBlob;
namespace BTCPayServer.Fido2
{
@ -45,7 +46,7 @@ namespace BTCPayServer.Fido2
var existingKeys =
user.Fido2Credentials
.Where(credential => credential.Type == Fido2Credential.CredentialType.FIDO2)
.Select(c => c.GetFido2Blob().Descriptor).ToList();
.Select(c => c.GetFido2Blob().Descriptor?.ToFido2()).ToList();
// 3. Create options
var authenticatorSelection = new AuthenticatorSelection
@ -57,14 +58,7 @@ namespace BTCPayServer.Fido2
var exts = new AuthenticationExtensionsClientInputs()
{
Extensions = true,
UserVerificationIndex = true,
Location = true,
UserVerificationMethod = true,
BiometricAuthenticatorPerformanceBounds = new AuthenticatorBiometricPerfBounds
{
FAR = float.MaxValue,
FRR = float.MaxValue
},
UserVerificationMethod = true
};
var options = _fido2.RequestNewCredential(
@ -81,7 +75,7 @@ namespace BTCPayServer.Fido2
try
{
var attestationResponse = JObject.Parse(data).ToObject<AuthenticatorAttestationRawResponse>();
var attestationResponse = System.Text.Json.JsonSerializer.Deserialize<AuthenticatorAttestationRawResponse>(data);
await using var dbContext = _contextFactory.CreateContext();
var user = await dbContext.Users.Include(applicationUser => applicationUser.Fido2Credentials)
.FirstOrDefaultAsync(applicationUser => applicationUser.Id == userId);
@ -92,14 +86,14 @@ namespace BTCPayServer.Fido2
// 2. Verify and make the credentials
var success =
await _fido2.MakeNewCredentialAsync(attestationResponse, options, args => Task.FromResult(true));
await _fido2.MakeNewCredentialAsync(attestationResponse, options, (args, cancellation) => Task.FromResult(true));
// 3. Store the credentials in db
var newCredential = new Fido2Credential() { Name = name, ApplicationUserId = userId };
newCredential.SetBlob(new Fido2CredentialBlob()
{
Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId),
Descriptor = new DescriptorClass(success.Result.CredentialId),
PublicKey = success.Result.PublicKey,
UserHandle = success.Result.User.Id,
SignatureCounter = success.Result.Counter,
@ -158,21 +152,13 @@ namespace BTCPayServer.Fido2
}
var existingCredentials = user.Fido2Credentials
.Where(credential => credential.Type == Fido2Credential.CredentialType.FIDO2)
.Select(c => c.GetFido2Blob().Descriptor)
.Select(c => c.GetFido2Blob().Descriptor?.ToFido2())
.ToList();
var exts = new AuthenticationExtensionsClientInputs()
{
SimpleTransactionAuthorization = "FIDO",
GenericTransactionAuthorization = new TxAuthGenericArg
{
ContentType = "text/plain",
Content = new byte[] { 0x46, 0x49, 0x44, 0x4F }
},
UserVerificationIndex = true,
Location = true,
UserVerificationMethod = true,
Extensions = true,
AppID = _fido2Configuration.Origin
AppID = _fido2Configuration.Origins.First()
};
// 3. Create options
@ -206,7 +192,7 @@ namespace BTCPayServer.Fido2
// 5. Make the assertion
var res = await _fido2.MakeAssertionAsync(response, options, credential.Item2.PublicKey,
credential.Item2.SignatureCounter, x => Task.FromResult(true));
credential.Item2.SignatureCounter, (x, cancellationToken) => Task.FromResult(true));
// 6. Store the updated counter
credential.Item2.SignatureCounter = res.Counter;

View file

@ -1,3 +1,4 @@
using System;
using Fido2NetLib;
using Fido2NetLib.Objects;
using Newtonsoft.Json;
@ -6,7 +7,84 @@ namespace BTCPayServer.Fido2.Models
{
public class Fido2CredentialBlob
{
public PublicKeyCredentialDescriptor Descriptor { get; set; }
public class Base64UrlConverter : JsonConverter<byte[]>
{
private readonly Required _requirement = Required.DisallowNull;
public Base64UrlConverter()
{
}
public Base64UrlConverter(Required required = Required.DisallowNull)
{
_requirement = required;
}
public override void WriteJson(JsonWriter writer, byte[] value, JsonSerializer serializer)
{
writer.WriteValue(Base64Url.Encode(value));
}
public override byte[] ReadJson(JsonReader reader, Type objectType, byte[] existingValue, bool hasExistingValue, JsonSerializer serializer)
{
byte[] ret = null;
if (null == reader.Value && _requirement == Required.AllowNull)
return ret;
if (null == reader.Value)
throw new Fido2VerificationException("json value must not be null");
if (Type.GetType("System.String") != reader.ValueType)
throw new Fido2VerificationException("json valuetype must be string");
try
{
ret = Base64Url.Decode((string)reader.Value);
}
catch (FormatException ex)
{
throw new Fido2VerificationException("json value must be valid base64 encoded string", ex);
}
return ret;
}
}
public class DescriptorClass
{
public DescriptorClass(byte[] credentialId)
{
Id = credentialId;
}
public DescriptorClass()
{
}
/// <summary>
/// This member contains the type of the public key credential the caller is referring to.
/// </summary>
[JsonProperty("type")]
public string Type { get; set; } = "public-key";
/// <summary>
/// This member contains the credential ID of the public key credential the caller is referring to.
/// </summary>
[JsonConverter(typeof(Base64UrlConverter))]
[JsonProperty("id")]
public byte[] Id { get; set; }
/// <summary>
/// This OPTIONAL member contains a hint as to how the client might communicate with the managing authenticator of the public key credential the caller is referring to.
/// </summary>
[JsonProperty("transports", NullValueHandling = NullValueHandling.Ignore)]
public string[] Transports { get; set; }
public PublicKeyCredentialDescriptor ToFido2()
{
var str = JsonConvert.SerializeObject(this);
return System.Text.Json.JsonSerializer.Deserialize<PublicKeyCredentialDescriptor>(str);
}
}
public DescriptorClass Descriptor { get; set; }
[JsonConverter(typeof(Base64UrlConverter))]
public byte[] PublicKey { get; set; }
[JsonConverter(typeof(Base64UrlConverter))]

View file

@ -1,4 +1,5 @@
using Fido2NetLib;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Fido2.Models
{
@ -7,7 +8,7 @@ namespace BTCPayServer.Fido2.Models
public string UserId { get; set; }
public bool RememberMe { get; set; }
public AssertionOptions Data { get; set; }
public string Data { get; set; }
public string Response { get; set; }
}
}

View file

@ -11,7 +11,6 @@ using BTCPayServer.Services.Invoices;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.WindowsAzure.Storage.Table;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

View file

@ -532,7 +532,8 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
{ "TRY", "btcturk" },
{ "UGX", "yadio"},
{ "RSD", "bitpay"},
{ "NGN", "bitnob"}
{ "NGN", "bitnob"},
{ "NOK", "barebitcoin"}
})
{
var r = new DefaultRules.Recommendation(rule.Key, rule.Value);
@ -587,6 +588,8 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
services.AddRateProvider<YadioRateProvider>();
services.AddRateProvider<BtcTurkRateProvider>();
services.AddRateProvider<FreeCurrencyRatesRateProvider>();
services.AddRateProvider<BitmyntRateProvider>();
services.AddRateProvider<BareBitcoinRateProvider>();
services.AddSingleton<InvoiceBlobMigratorHostedService>();
services.AddSingleton<IHostedService, InvoiceBlobMigratorHostedService>(o => o.GetRequiredService<InvoiceBlobMigratorHostedService>());

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
@ -22,14 +23,15 @@ using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using BTCPayServer.Storage.Models;
using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration;
using Fido2NetLib.Cbor;
using Fido2NetLib.Objects;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PeterO.Cbor;
using YamlDotNet.RepresentationModel;
using static BTCPayServer.Fido2.Models.Fido2CredentialBlob;
using LightningAddressData = BTCPayServer.Data.LightningAddressData;
namespace BTCPayServer.Hosting
@ -738,9 +740,9 @@ WHERE cte.""Id""=p.""Id""
fido2.SetBlob(new Fido2CredentialBlob()
{
SignatureCounter = (uint)u2FDevice.Counter,
PublicKey = CreatePublicKeyFromU2fRegistrationData(u2FDevice.PublicKey).EncodeToBytes(),
PublicKey = CreatePublicKeyFromU2fRegistrationData(u2FDevice.PublicKey).Encode(),
UserHandle = u2FDevice.KeyHandle,
Descriptor = new PublicKeyCredentialDescriptor(u2FDevice.KeyHandle),
Descriptor = new DescriptorClass(u2FDevice.KeyHandle),
CredType = "u2f"
});
@ -751,27 +753,29 @@ WHERE cte.""Id""=p.""Id""
await ctx.SaveChangesAsync();
}
//from https://github.com/abergs/fido2-net-lib/blob/0fa7bb4b4a1f33f46c5f7ca4ee489b47680d579b/Test/ExistingU2fRegistrationDataTests.cs#L70
private static CBORObject CreatePublicKeyFromU2fRegistrationData(byte[] publicKeyData)
private static CborMap CreatePublicKeyFromU2fRegistrationData(byte[] publicKeyData)
{
if (publicKeyData.Length != 65)
{
throw new ArgumentException("u2f public key must be 65 bytes", nameof(publicKeyData));
}
var x = new byte[32];
var y = new byte[32];
Buffer.BlockCopy(publicKeyData, 1, x, 0, 32);
Buffer.BlockCopy(publicKeyData, 33, y, 0, 32);
var point = new ECPoint
{
X = x,
Y = y,
};
var coseKey = CBORObject.NewMap();
var coseKey = new CborMap
{
{ (long)COSE.KeyCommonParameter.KeyType, (long)COSE.KeyType.EC2 },
{ (long)COSE.KeyCommonParameter.Alg, -7L },
coseKey.Add(COSE.KeyCommonParameter.KeyType, COSE.KeyType.EC2);
coseKey.Add(COSE.KeyCommonParameter.Alg, -7);
{ (long)COSE.KeyTypeParameter.Crv, (long)COSE.EllipticCurve.P256 },
coseKey.Add(COSE.KeyTypeParameter.Crv, COSE.EllipticCurve.P256);
coseKey.Add(COSE.KeyTypeParameter.X, x);
coseKey.Add(COSE.KeyTypeParameter.Y, y);
{ (long)COSE.KeyTypeParameter.X, point.X },
{ (long)COSE.KeyTypeParameter.Y, point.Y }
};
return coseKey;
}

View file

@ -122,8 +122,7 @@ namespace BTCPayServer.Hosting
})
.AddCachedMetadataService(config =>
{
//They'll be used in a "first match wins" way in the order registered
config.AddStaticMetadataRepository();
config.AddFidoMetadataRepository();
});
var descriptor = services.Single(descriptor => descriptor.ServiceType == typeof(Fido2Configuration));
services.Remove(descriptor);
@ -133,7 +132,7 @@ namespace BTCPayServer.Hosting
return new Fido2Configuration()
{
ServerName = "BTCPay Server",
Origin = $"{httpContext.HttpContext.Request.Scheme}://{httpContext.HttpContext.Request.Host}",
Origins = new[] { $"{httpContext.HttpContext.Request.Scheme}://{httpContext.HttpContext.Request.Host}" }.ToHashSet(),
ServerDomain = httpContext.HttpContext.Request.Host.Host
};
});
@ -141,7 +140,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<UserLoginCodeService>();
services.AddSingleton<LnurlAuthService>();
services.AddSingleton<LightningAddressService>();
services.AddMvc(o =>
var mvcBuilder = services.AddMvc(o =>
{
o.Filters.Add(new XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.Deny));
o.Filters.Add(new XContentTypeOptionsAttribute("nosniff"));
@ -167,11 +166,14 @@ namespace BTCPayServer.Hosting
o.PageViewLocationFormats.Add("/{0}.cshtml");
})
.AddNewtonsoftJson()
.AddRazorRuntimeCompilation()
.AddPlugins(services, Configuration, LoggerFactory, bootstrapServiceProvider)
.AddDataAnnotationsLocalization()
.AddControllersAsServices();
#if !RAZOR_COMPILE_ON_BUILD
mvcBuilder.AddRazorRuntimeCompilation();
#endif
services.AddServerSideBlazor();
LowercaseTransformer.Register(services);

View file

@ -67,16 +67,14 @@ namespace BTCPayServer.PaymentRequest
var invoices = await _paymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id);
var contributions = _invoiceRepository.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
if (contributions.Total >= blob.Amount)
{
currentStatus = contributions.TotalSettled >= blob.Amount
? Client.Models.PaymentRequestData.PaymentRequestStatus.Completed
: Client.Models.PaymentRequestData.PaymentRequestStatus.Processing;
}
else
{
currentStatus = Client.Models.PaymentRequestData.PaymentRequestStatus.Pending;
}
currentStatus =
(PaidEnough: contributions.Total >= blob.Amount,
SettledEnough: contributions.TotalSettled >= blob.Amount) switch
{
{ SettledEnough: true } => Client.Models.PaymentRequestData.PaymentRequestStatus.Completed,
{ PaidEnough: true } => Client.Models.PaymentRequestData.PaymentRequestStatus.Processing,
_ => Client.Models.PaymentRequestData.PaymentRequestStatus.Pending
};
}
if (currentStatus != pr.Status)
@ -100,7 +98,7 @@ namespace BTCPayServer.PaymentRequest
var amountDue = blob.Amount - paymentStats.Total;
var pendingInvoice = invoices.OrderByDescending(entity => entity.InvoiceTime)
.FirstOrDefault(entity => entity.Status == InvoiceStatus.New);
return new ViewPaymentRequestViewModel(pr)
{
Archived = pr.Archived,
@ -121,8 +119,7 @@ namespace BTCPayServer.PaymentRequest
var state = entity.GetInvoiceState();
var payments = ViewPaymentRequestViewModel.PaymentRequestInvoicePayment.GetViewModels(entity, _displayFormatter, _transactionLinkProviders, _handlers);
if (state.Status == InvoiceStatus.Invalid ||
state.Status == InvoiceStatus.Expired && !payments.Any())
if (state.Status is InvoiceStatus.Invalid or InvoiceStatus.Expired && payments.Count is 0)
return null;
return new ViewPaymentRequestViewModel.PaymentRequestInvoice

View file

@ -23,7 +23,6 @@ using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using static Org.BouncyCastle.Math.EC.ECCurve;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Payments.Bitcoin

View file

@ -17,7 +17,6 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using crypto;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBitcoin;

View file

@ -144,6 +144,8 @@ retry:
return _memoryCache.GetOrCreateAsync(GetCacheKey(invoiceId), async (cacheEntry) =>
{
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
if (invoice is null)
return null;
cacheEntry.AbsoluteExpiration = GetExpiration(invoice);
return invoice;
})!;

View file

@ -2,8 +2,10 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using Dapper;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using Npgsql;
namespace BTCPayServer.Payments.PayJoin
{
@ -16,20 +18,6 @@ namespace BTCPayServer.Payments.PayJoin
_dbContextFactory = dbContextFactory;
}
public async Task<bool> TryLock(OutPoint outpoint)
{
using var ctx = _dbContextFactory.CreateContext();
ctx.PayjoinLocks.Add(new PayjoinLock() { Id = outpoint.ToString() });
try
{
return await ctx.SaveChangesAsync() == 1;
}
catch (DbUpdateException)
{
return false;
}
}
public async Task<bool> TryUnlock(params OutPoint[] outPoints)
{
using var ctx = _dbContextFactory.CreateContext();
@ -48,29 +36,33 @@ namespace BTCPayServer.Payments.PayJoin
}
}
public async Task<bool> TryLockInputs(OutPoint[] outPoints)
private async Task<bool> TryLockInputs(string[] ids)
{
using var ctx = _dbContextFactory.CreateContext();
foreach (OutPoint outPoint in outPoints)
{
ctx.PayjoinLocks.Add(new PayjoinLock()
{
// Random flag so it does not lock same id
// as the lock utxo
Id = "K-" + outPoint.ToString()
});
}
var connection = ctx.Database.GetDbConnection();
try
{
return await ctx.SaveChangesAsync() == outPoints.Length;
await connection.ExecuteAsync("""
INSERT INTO "PayjoinLocks"("Id")
SELECT * FROM unnest(@ids)
""", new { ids });
return true;
}
catch (DbUpdateException)
catch (Npgsql.PostgresException ex) when (ex.SqlState == PostgresErrorCodes.UniqueViolation)
{
return false;
}
}
public Task<bool> TryLock(OutPoint outpoint)
=> TryLockInputs([outpoint.ToString()]);
public Task<bool> TryLockInputs(OutPoint[] outPoints)
=> TryLockInputs(outPoints.Select(o => "K-" + o.ToString()).ToArray());
public async Task<HashSet<OutPoint>> FindLocks(OutPoint[] outpoints)
{
var outPointsStr = outpoints.Select(o => o.ToString());

View file

@ -43,6 +43,10 @@ namespace BTCPayServer.Services.Apps
private readonly DisplayFormatter _displayFormatter;
private readonly StoreRepository _storeRepository;
public CurrencyNameTable Currencies => _Currencies;
private readonly string[] _paidStatuses = [
InvoiceStatus.Processing.ToString(),
InvoiceStatus.Settled.ToString()
];
public AppService(
IEnumerable<AppBaseType> apps,
@ -86,11 +90,7 @@ namespace BTCPayServer.Services.Apps
{
if (GetAppType(appData.AppType) is not IHasItemStatsAppType salesType)
throw new InvalidOperationException("This app isn't a SalesAppBaseType");
var paidInvoices = await GetInvoicesForApp(_InvoiceRepository, appData, null,
[
InvoiceStatus.Processing.ToString(),
InvoiceStatus.Settled.ToString()
]);
var paidInvoices = await GetInvoicesForApp(_InvoiceRepository, appData, null, _paidStatuses);
return await salesType.GetItemStats(appData, paidInvoices);
}
@ -132,8 +132,7 @@ namespace BTCPayServer.Services.Apps
{
if (GetAppType(app.AppType) is not IHasSaleStatsAppType salesType)
throw new InvalidOperationException("This app isn't a SalesAppBaseType");
var paidInvoices = await GetInvoicesForApp(_InvoiceRepository, app, DateTimeOffset.UtcNow - TimeSpan.FromDays(numberOfDays));
var paidInvoices = await GetInvoicesForApp(_InvoiceRepository, app, DateTimeOffset.UtcNow - TimeSpan.FromDays(numberOfDays), _paidStatuses);
return await salesType.GetSalesStats(app, paidInvoices, numberOfDays);
}

View file

@ -267,9 +267,9 @@ namespace BTCPayServer.Services.Invoices
}
public const int InternalTagSupport_Version = 1;
public const int GreenfieldInvoices_Version = 2;
public const int LeanInvoices_Version = 3;
public const int Lastest_Version = 3;
public int Version { get; set; }
public const int LeanInvoices_Version = 3;
public const int Lastest_Version = 3;
public int Version { get; set; }
[JsonIgnore]
public string Id { get; set; }
[JsonIgnore]
@ -349,7 +349,7 @@ namespace BTCPayServer.Services.Invoices
ArgumentNullException.ThrowIfNull(pair);
#pragma warning disable CS0618 // Type or member is obsolete
if (pair.Right == Currency && Rates.TryGetValue(pair.Left, out var rate)) // Fast lane
return rate;
return rate;
#pragma warning restore CS0618 // Type or member is obsolete
var rule = GetRateRules().GetRuleFor(pair);
rule.Reevaluate();
@ -802,33 +802,40 @@ namespace BTCPayServer.Services.Invoices
}
public record InvoiceState(InvoiceStatus Status, InvoiceExceptionStatus ExceptionStatus)
{
public InvoiceState(string status, string exceptionStatus):
public InvoiceState(string status, string exceptionStatus) :
this(Enum.Parse<InvoiceStatus>(status), exceptionStatus switch { "None" or "" or null => InvoiceExceptionStatus.None, _ => Enum.Parse<InvoiceExceptionStatus>(exceptionStatus) })
{
}
public bool CanMarkComplete()
public bool CanMarkComplete() => (Status, ExceptionStatus) is
{
return Status is InvoiceStatus.New or InvoiceStatus.Processing or InvoiceStatus.Expired or InvoiceStatus.Invalid ||
(Status != InvoiceStatus.Settled && ExceptionStatus == InvoiceExceptionStatus.Marked);
Status: InvoiceStatus.New or InvoiceStatus.Processing or InvoiceStatus.Expired or InvoiceStatus.Invalid
}
or
{
Status: not InvoiceStatus.Settled,
ExceptionStatus: InvoiceExceptionStatus.Marked
};
public bool CanMarkInvalid()
public bool CanMarkInvalid() => (Status, ExceptionStatus) is
{
return Status is InvoiceStatus.New or InvoiceStatus.Processing or InvoiceStatus.Expired ||
(Status != InvoiceStatus.Invalid && ExceptionStatus == InvoiceExceptionStatus.Marked);
Status: InvoiceStatus.New or InvoiceStatus.Processing or InvoiceStatus.Expired
}
or
{
Status: not InvoiceStatus.Invalid,
ExceptionStatus: InvoiceExceptionStatus.Marked
};
public bool CanRefund()
public bool CanRefund() => (Status, ExceptionStatus) is
{
return
Status == InvoiceStatus.Settled ||
(Status == InvoiceStatus.Expired &&
(ExceptionStatus == InvoiceExceptionStatus.PaidLate ||
ExceptionStatus == InvoiceExceptionStatus.PaidOver ||
ExceptionStatus == InvoiceExceptionStatus.PaidPartial)) ||
Status == InvoiceStatus.Invalid;
Status: InvoiceStatus.Settled or InvoiceStatus.Invalid
}
or
{
Status: InvoiceStatus.Expired,
ExceptionStatus: InvoiceExceptionStatus.PaidLate or InvoiceExceptionStatus.PaidOver or InvoiceExceptionStatus.PaidPartial
};
public override string ToString()
{

View file

@ -1,6 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.WindowsAzure.Storage;
namespace BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration
{
@ -10,7 +9,7 @@ namespace BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration
{
try
{
CloudStorageAccount.Parse(value as string);
new Azure.Storage.Blobs.BlobClient(value as string, "unusedcontainer", "unusedblob");
return ValidationResult.Success;
}
catch (Exception e)

View file

@ -32,7 +32,8 @@
<div class="sticky-header">
<h2 text-translate="true">@ViewData["Title"]</h2>
<div>
<button id="page-primary" type="submit" class="btn btn-primary order-sm-1">Save</button>
<button id="page-primary" type="submit" class="btn btn-primary order-sm-1">Save</button>
<a class="btn btn-secondary" asp-action="ListInvoices" asp-controller="UIInvoice" asp-route-storeId="@Model.StoreId" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
@if (Model.Archived)
{
<button type="submit" class="btn btn-outline-secondary" name="Archived" value="False" text-translate="true">Unarchive</button>
@ -345,7 +346,6 @@
</form>
<div class="d-grid d-sm-flex flex-wrap gap-3 mt-3">
<a class="btn btn-secondary" asp-action="ListInvoices" asp-controller="UIInvoice" asp-route-storeId="@Model.StoreId" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
<form method="post" asp-controller="UIApps" asp-action="ToggleArchive" asp-route-appId="@Model.AppId" permission="@Policies.CanModifyStoreSettings">
<button type="submit" class="w-100 btn btn-outline-secondary" id="btn-archive-toggle">
@if (Model.Archived)

View file

@ -31,7 +31,8 @@
<div class="sticky-header">
<h2 text-translate="true">@ViewData["Title"]</h2>
<div>
<button id="page-primary" type="submit" class="btn btn-primary order-sm-1">Save</button>
<button id="page-primary" type="submit" class="btn btn-primary order-sm-1">Save</button>
<a class="btn btn-secondary" asp-action="ListInvoices" asp-controller="UIInvoice" asp-route-storeId="@Model.StoreId" asp-route-searchterm="@Model.SearchTerm" text-translate="true">Invoices</a>
@if (Model.Archived)
{
<button type="submit" class="btn btn-outline-secondary" name="Archived" value="False" text-translate="true">Unarchive</button>
@ -293,7 +294,6 @@
</form>
<div class="d-grid d-sm-flex flex-wrap gap-3 mt-3">
<a class="btn btn-secondary" asp-action="ListInvoices" asp-controller="UIInvoice" asp-route-storeId="@Model.StoreId" asp-route-searchterm="@Model.SearchTerm" text-translate="true">Invoices</a>
<form method="post" asp-controller="UIApps" asp-action="ToggleArchive" asp-route-appId="@Model.Id">
<button type="submit" class="w-100 btn btn-outline-secondary" id="btn-archive-toggle" permission="@Policies.CanModifyStoreSettings">
@if (Model.Archived)

View file

@ -1,3 +1,4 @@
@using Newtonsoft.Json.Linq
@model BTCPayServer.Fido2.Models.LoginWithFido2ViewModel
<div class="twoFaBox">
@ -24,7 +25,7 @@
<script>
document.getElementById('btn-retry').addEventListener('click', () => window.location.reload())
// send to server for registering
window.makeAssertionOptions = @Safe.Json(Model.Data);
window.makeAssertionOptions = @Safe.Json(JObject.Parse(Model.Data));
</script>
<script src="~/js/webauthn/helpers.js" asp-append-version="true"></script>
<script src="~/js/webauthn/login.js" asp-append-version="true"></script>

View file

@ -1,3 +1,4 @@
@using Newtonsoft.Json.Linq
@model Fido2NetLib.CredentialCreateOptions
@{
ViewData.SetActivePage(ManageNavPages.TwoFactorAuthentication, StringLocalizer["Register your security device"]);
@ -42,7 +43,7 @@
<script>
document.getElementById('btn-retry').addEventListener('click', function () { window.location.reload() });
// send to server for registering
window.makeCredentialOptions = @Json.Serialize(Model);
window.makeCredentialOptions = @Json.Serialize(JToken.Parse(Model.ToJson()));
</script>
<script src="~/js/webauthn/helpers.js"></script>
<script src="~/js/webauthn/register.js"></script>

View file

@ -37,11 +37,6 @@
<input asp-for="Bucket" class="form-control" />
<span asp-validation-for="Bucket" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Email" class="form-label"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary" name="command" value="Save" text-translate="true">Save</button>
<a asp-action="Storage" asp-route-forceChoice="true" class="ms-2" text-translate="true">Change Storage provider</a>

View file

@ -33,16 +33,12 @@
: ''
});
delegate('click', '#Presets_InStore', e => {
$("#CheckoutV2Settings").addClass('show');
$("#ClassicCheckoutSettings").removeClass('show');
$("#CheckNFC").removeClass('d-none');
$("#PlaySoundOnPayment").prop('checked', true);
$("#ShowPayInWalletButton").prop('checked', false);
$("#ShowStoreHeader").prop('checked', false);
});
delegate('click', '#Presets_Online', e => {
$("#CheckoutV2Settings").addClass('show');
$("#ClassicCheckoutSettings").removeClass('show');
$("#CheckNFC").addClass('d-none');
$("#PlaySoundOnPayment").prop('checked', false);
$("#ShowPayInWalletButton").prop('checked', true);
@ -135,7 +131,7 @@
<div id="CheckNFC" class="form-group d-none">
<button type="button" class="btn btn-outline-secondary" text-translate="true">Check if NFC is supported and enabled on this device</button>
</div>
<div class="checkout-settings collapse show" id="CheckoutV2Settings">
<div class="checkout-settings">
<div class="form-group">
<label asp-for="DisplayExpirationTimer" class="form-label"></label>
<div class="input-group">

View file

@ -1,7 +1,5 @@
@model WalletPSBTReadyViewModel
<script src="~/js/wallet/wallet-camera-scanner.js" asp-append-version="true"></script>
<script src="~/js/wallet/WalletSend.js" asp-append-version="true"></script>
@if (Model.CanCalculateBalance)
{
<p class="lead text-center text-secondary">

View file

@ -40,7 +40,7 @@ async function verifyAssertionWithServer(assertedCredential) {
extensions: assertedCredential.getClientExtensionResults(),
response: {
authenticatorData: coerceToBase64Url(authData),
clientDataJson: coerceToBase64Url(clientDataJSON),
clientDataJSON: coerceToBase64Url(clientDataJSON),
signature: coerceToBase64Url(sig)
}
};

View file

@ -49,8 +49,8 @@ async function registerNewCredential(newCredential) {
type: newCredential.type,
extensions: newCredential.getClientExtensionResults(),
response: {
AttestationObject: coerceToBase64Url(attestationObject),
clientDataJson: coerceToBase64Url(clientDataJSON)
attestationObject: coerceToBase64Url(attestationObject),
clientDataJSON: coerceToBase64Url(clientDataJSON)
}
};

View file

@ -47,13 +47,7 @@
"description": "Revoke the API key of a target user so that it cannot be used anymore",
"parameters": [
{
"name": "idOrEmail",
"in": "path",
"required": true,
"description": "The target user's id or email",
"schema": {
"type": "string"
}
"$ref": "#/components/parameters/UserIdOrEmail"
},
{
"name": "apikey",
@ -244,13 +238,7 @@
"description": "Create a new API Key for a user",
"parameters": [
{
"name": "idOrEmail",
"in": "path",
"required": true,
"description": "The target user's id or email",
"schema": {
"type": "string"
}
"$ref": "#/components/parameters/UserIdOrEmail"
}
],
"responses": {

View file

@ -42,6 +42,15 @@
"schema": {
"type": "string"
}
},
"UserIdOrEmail": {
"name": "idOrEmail",
"in": "path",
"required": true,
"description": "The user's id or email",
"schema": {
"type": "string"
}
}
},
"schemas": {

View file

@ -25,10 +25,10 @@
}
},
"403": {
"description": "If you are authenticated but forbidden to view the specified store"
"description": "If you are authenticated but forbidden to view the specified store's users"
},
"404": {
"description": "The key is not found for this store"
"description": "The store could not be found"
}
},
"security": [
@ -69,7 +69,7 @@
"description": "The user was added"
},
"400": {
"description": "A list of errors that occurred when creating the store",
"description": "A list of errors that occurred when adding the store user",
"content": {
"application/json": {
"schema": {
@ -79,7 +79,10 @@
}
},
"403": {
"description": "If you are authenticated but forbidden to add new stores"
"description": "If you are authenticated but forbidden to add new store users"
},
"404": {
"description": "The store or user could not be found"
},
"409": {
"description": "Error code: `duplicate-store-user-role`. Removing this user would result in the store having no owner.",
@ -103,6 +106,63 @@
}
},
"/api/v1/stores/{storeId}/users/{idOrEmail}": {
"put": {
"tags": [
"Stores (Users)"
],
"summary": "Updates a store user",
"description": "Updates a store user",
"operationId": "Stores_UpdateStoreUser",
"parameters": [
{
"$ref": "#/components/parameters/StoreId"
},
{
"$ref": "#/components/parameters/UserIdOrEmail"
}
],
"requestBody": {
"x-name": "request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StoreUserData"
}
}
},
"required": true,
"x-position": 1
},
"responses": {
"200": {
"description": "The user was updated"
},
"400": {
"description": "A list of errors that occurred when updating the store user",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationProblemDetails"
}
}
}
},
"403": {
"description": "If you are authenticated but forbidden to update store users"
},
"404": {
"description": "The store or user could not be found"
}
},
"security": [
{
"API_Key": [
"btcpay.store.canmodifystoresettings"
],
"Basic": []
}
]
},
"delete": {
"tags": [
"Stores (Users)"
@ -115,13 +175,7 @@
"$ref": "#/components/parameters/StoreId"
},
{
"name": "idOrEmail",
"in": "path",
"required": true,
"description": "The user's id or email",
"schema": {
"type": "string"
}
"$ref": "#/components/parameters/UserIdOrEmail"
}
],
"responses": {
@ -129,7 +183,7 @@
"description": "The user has been removed"
},
"400": {
"description": "A list of errors that occurred when removing the store",
"description": "A list of errors that occurred when removing the store user",
"content": {
"application/json": {
"schema": {
@ -149,10 +203,10 @@
}
},
"403": {
"description": "If you are authenticated but forbidden to remove the specified store"
"description": "If you are authenticated but forbidden to remove the specified store user"
},
"404": {
"description": "The key is not found for this store"
"description": "The store or user could not be found"
}
},
"security": [
@ -186,8 +240,23 @@
},
"role": {
"type": "string",
"description": "The role of the user. Default roles are `Owner` and `Guest`",
"description": "The role of the user. Default roles are `Owner`, `Manager`, `Employee` and `Guest`",
"nullable": false
},
"email": {
"type": "string",
"description": "The email of the user",
"nullable": true
},
"name": {
"type": "string",
"description": "The name of the user",
"nullable": true
},
"imageUrl": {
"type": "string",
"description": "The profile picture URL of the user",
"nullable": true
}
}
}

View file

@ -330,13 +330,7 @@
"description": "Get 1 user by ID or Email.",
"parameters": [
{
"name": "idOrEmail",
"in": "path",
"required": true,
"description": "The ID or email of the user to load",
"schema": {
"type": "string"
}
"$ref": "#/components/parameters/UserIdOrEmail"
}
],
"responses": {
@ -378,13 +372,7 @@
"description": "Delete a user.\n\nMust be an admin to perform this operation.\n\nAttempting to delete the only admin user will not succeed.\n\nAll data associated with the user will be deleted as well if the operation succeeds.",
"parameters": [
{
"name": "idOrEmail",
"in": "path",
"required": true,
"description": "The ID or email of the user to be deleted",
"schema": {
"type": "string"
}
"$ref": "#/components/parameters/UserIdOrEmail"
}
],
"responses": {
@ -421,13 +409,7 @@
"description": "Lock or unlock a user.\n\nMust be an admin to perform this operation.\n\nAttempting to lock the only admin user will not succeed.",
"parameters": [
{
"name": "idOrEmail",
"in": "path",
"required": true,
"description": "The ID of the user to be un/locked",
"schema": {
"type": "string"
}
"$ref": "#/components/parameters/UserIdOrEmail"
}
],
"requestBody": {
@ -474,13 +456,7 @@
"description": "Approve or unapprove a user.\n\nMust be an admin to perform this operation.\n\nAttempting to (un)approve a user for which this requirement does not exist will not succeed.",
"parameters": [
{
"name": "idOrEmail",
"in": "path",
"required": true,
"description": "The ID of the user to be un/approved",
"schema": {
"type": "string"
}
"$ref": "#/components/parameters/UserIdOrEmail"
}
],
"requestBody": {

View file

@ -12,8 +12,4 @@
<Configurations>Debug;Release</Configurations>
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug' And '$(RazorCompileOnBuild)' != 'true' And '$(DotNetWatchBuild)' != 'true' And '$(DesignTimeBuild)' != 'true'">
<RazorCompileOnBuild>false</RazorCompileOnBuild>
</PropertyGroup>
</Project>

View file

@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0.203-bookworm-slim AS builder
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0.404-bookworm-slim AS builder
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
WORKDIR /source
COPY nuget.config nuget.config
@ -21,7 +21,7 @@ ARG CONFIGURATION_NAME=Release
ARG GIT_COMMIT
RUN cd BTCPayServer && dotnet publish -p:GitCommit=${GIT_COMMIT} --output /app/ --configuration ${CONFIGURATION_NAME}
FROM mcr.microsoft.com/dotnet/aspnet:8.0.3-bookworm-slim
FROM mcr.microsoft.com/dotnet/aspnet:8.0.11-bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends iproute2 openssh-client \
&& rm -rf /var/lib/apt/lists/*