From e65850b1eb3f87189c54988263c121f14b24f36b Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Thu, 2 May 2019 18:56:01 +0900 Subject: [PATCH 001/243] Refactor Send money from ledger using PSBT --- BTCPayServer/Controllers/WalletsController.cs | 147 +++++++----------- .../Services/HardwareWalletService.cs | 30 +++- 2 files changed, 78 insertions(+), 99 deletions(-) diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index b61038826..8b271a69b 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -421,6 +421,7 @@ namespace BTCPayServer.Controllers var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId())); var derivationScheme = GetPaymentMethod(walletId, storeData).DerivationStrategyBase; + var psbtRequest = new CreatePSBTRequest(); var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); using (var normalOperationTimeout = new CancellationTokenSource()) @@ -439,48 +440,52 @@ namespace BTCPayServer.Controllers throw new FormatException("Invalid value for crypto code"); } - BitcoinAddress destinationAddress = null; + CreatePSBTDestination destinationPSBT = null; if (destination != null) { try { - destinationAddress = BitcoinAddress.Create(destination.Trim(), network.NBitcoinNetwork); + destinationPSBT = new CreatePSBTDestination() + { + Destination = BitcoinAddress.Create(destination.Trim(), network.NBitcoinNetwork) + }; + psbtRequest.Destinations.Add(destinationPSBT); } catch { } - if (destinationAddress == null) + if (destinationPSBT == null) throw new FormatException("Invalid value for destination"); } - FeeRate feeRateValue = null; + if (feeRate != null) { + psbtRequest.FeePreference = new FeePreference(); try { - feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1); + psbtRequest.FeePreference.ExplicitFeeRate = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1); } catch { } - if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero) + if (psbtRequest.FeePreference.ExplicitFeeRate == null || + psbtRequest.FeePreference.ExplicitFeeRate.FeePerK <= Money.Zero) throw new FormatException("Invalid value for fee rate"); } - Money amountBTC = null; if (amount != null) { try { - amountBTC = Money.Parse(amount); + destinationPSBT.Amount = Money.Parse(amount); } catch { } - if (amountBTC == null || amountBTC <= Money.Zero) + if (destinationPSBT.Amount == null || destinationPSBT.Amount <= Money.Zero) throw new FormatException("Invalid value for amount"); } - bool subsctractFeesValue = false; if (substractFees != null) { try { - subsctractFeesValue = bool.Parse(substractFees); + destinationPSBT.SubstractFees = bool.Parse(substractFees); } catch { throw new FormatException("Invalid value for subtract fees"); } } @@ -493,17 +498,25 @@ namespace BTCPayServer.Controllers if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary)) throw new Exception($"{network.CryptoCode}: not started or fully synched"); var strategy = GetDirectDerivationStrategy(derivationScheme); - var wallet = _walletProvider.GetWallet(network); - var change = wallet.GetChangeAddressAsync(derivationScheme); - var keypaths = new Dictionary(); - List availableCoins = new List(); - foreach (var c in await wallet.GetUnspentCoins(derivationScheme)) - { - keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath); - availableCoins.Add(c.Coin); - } + var nbx = ExplorerClientProvider.GetExplorerClient(network); - var changeAddress = await change; + if (noChange) + { + psbtRequest.ExplicitChangeAddress = destinationPSBT.Destination; + } + var psbt = (await nbx.CreatePSBTAsync(derivationScheme, psbtRequest, normalOperationTimeout.Token))?.PSBT; + if (psbt == null) + throw new Exception("You need to update your version of NBXplorer"); + + if (network.MinFee != null) + { + psbt.TryGetFee(out var fee); + if (fee < network.MinFee) + { + psbtRequest.FeePreference = new FeePreference() { ExplicitFee = network.MinFee }; + psbt = (await nbx.CreatePSBTAsync(derivationScheme, psbtRequest, normalOperationTimeout.Token)).PSBT; + } + } var storeBlob = storeData.GetStoreBlob(); var paymentId = new Payments.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike); @@ -520,97 +533,41 @@ namespace BTCPayServer.Controllers storeData.SetStoreBlob(storeBlob); await Repository.UpdateStore(storeData); } -retry: - var send = new[] { ( - destination: destinationAddress as IDestination, - amount: amountBTC, - substractFees: subsctractFeesValue) }; - foreach (var element in send) + // NBX only know the path relative to the account xpub. + // Here we rebase the hd_keys in the PSBT to have a keypath relative to the root HD so the wallet can sign + // Note that the fingerprint of the hd keys are now 0, which is wrong + // However, hardware wallets does not give a damn, and sometimes does not even allow us to get this fingerprint anyway. + foreach (var o in psbt.Inputs.OfType().Concat(psbt.Outputs)) { - if (element.destination == null) - throw new ArgumentNullException(nameof(element.destination)); - if (element.amount == null) - throw new ArgumentNullException(nameof(element.amount)); - if (element.amount <= Money.Zero) - throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero"); - } - - TransactionBuilder builder = network.NBitcoinNetwork.CreateTransactionBuilder(); - builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee; - builder.AddCoins(availableCoins); - - foreach (var element in send) - { - builder.Send(element.destination, element.amount); - if (element.substractFees) - builder.SubtractFees(); - } - - builder.SetChange(changeAddress.Item1); - - if (network.MinFee == null) - { - builder.SendEstimatedFees(feeRateValue); - } - else - { - var estimatedFee = builder.EstimateFees(feeRateValue); - if (network.MinFee > estimatedFee) - builder.SendFees(network.MinFee); - else - builder.SendEstimatedFees(feeRateValue); - } - var unsigned = builder.BuildTransaction(false); - - var hasChange = unsigned.Outputs.Any(o => o.ScriptPubKey == changeAddress.Item1.ScriptPubKey); - if (noChange && hasChange) - { - availableCoins = builder.FindSpentCoins(unsigned).Cast().ToList(); - amountBTC = builder.FindSpentCoins(unsigned).Select(c => c.TxOut.Value).Sum(); - subsctractFeesValue = true; - goto retry; - } - - var usedCoins = builder.FindSpentCoins(unsigned); - - Dictionary parentTransactions = new Dictionary(); - - if (!strategy.Segwit) - { - var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet(); - var explorer = ExplorerClientProvider.GetExplorerClient(network); - var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList(); - foreach (var getTransactionAsync in getTransactionAsyncs) + foreach (var keypath in o.HDKeyPaths.ToList()) { - var tx = (await getTransactionAsync.Op); - if (tx == null) - throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found"); - parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction); + var newKeyPath = foundKeyPath.Derive(keypath.Value.Item2); + o.HDKeyPaths.Remove(keypath.Key); + o.HDKeyPaths.Add(keypath.Key, Tuple.Create(default(HDFingerprint), newKeyPath)); } } - signTimeout.CancelAfter(TimeSpan.FromMinutes(5)); - var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest + psbt = await hw.SignTransactionAsync(psbt, signTimeout.Token); + if(!psbt.TryFinalize(out var errors)) { - InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash), - InputCoin = c, - KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]), - PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey - }).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null, signTimeout.Token); + throw new Exception($"Error while finalizing the transaction ({new PSBTException(errors).ToString()})"); + } + var transaction = psbt.ExtractTransaction(); try { - var broadcastResult = await wallet.BroadcastTransactionsAsync(new List() { transaction }); - if (!broadcastResult[0].Success) + var broadcastResult = await nbx.BroadcastAsync(transaction); + if (!broadcastResult.Success) { - throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}"); + throw new Exception($"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}"); } } catch (Exception ex) { throw new Exception("Error while broadcasting: " + ex.Message); } + var wallet = _walletProvider.GetWallet(network); wallet.InvalidateCache(derivationScheme); result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() }; } diff --git a/BTCPayServer/Services/HardwareWalletService.cs b/BTCPayServer/Services/HardwareWalletService.cs index cc1a9a7e2..dbdb1ec07 100644 --- a/BTCPayServer/Services/HardwareWalletService.cs +++ b/BTCPayServer/Services/HardwareWalletService.cs @@ -164,17 +164,39 @@ namespace BTCPayServer.Services return foundKeyPath; } - public async Task SignTransactionAsync(SignatureRequest[] signatureRequests, - Transaction unsigned, - KeyPath changeKeyPath, + public async Task SignTransactionAsync(PSBT psbt, CancellationToken cancellationToken) { try { + var unsigned = psbt.GetGlobalTransaction(); + var changeKeyPath = psbt.Outputs.Where(o => o.HDKeyPaths.Any()) + .Select(o => o.HDKeyPaths.First().Value.Item2) + .FirstOrDefault(); + var signatureRequests = psbt + .Inputs + .Where(o => o.HDKeyPaths.Any()) + .Where(o => !o.PartialSigs.ContainsKey(o.HDKeyPaths.First().Key)) + .Select(i => new SignatureRequest() + { + InputCoin = i.GetSignableCoin(), + InputTransaction = i.NonWitnessUtxo, + KeyPath = i.HDKeyPaths.First().Value.Item2, + PubKey = i.HDKeyPaths.First().Key + }).ToArray(); var signedTransaction = await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken); if (signedTransaction == null) throw new Exception("The ledger failed to sign the transaction"); - return signedTransaction; + + psbt = psbt.Clone(); + foreach (var signature in signatureRequests) + { + var input = psbt.Inputs.FindIndexedInput(signature.InputCoin.Outpoint); + if (input == null) + continue; + input.PartialSigs.Add(signature.PubKey, signature.Signature); + } + return psbt; } catch (Exception ex) { From bac99deb6c466fb26ad2fa47bc00f7d1e4397fdf Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Thu, 2 May 2019 20:38:45 +0900 Subject: [PATCH 002/243] Do not run external integration if PR --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f1051b460..ffbf1980f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,7 +17,7 @@ jobs: docker-compose down --v docker-compose build TESTS_RUN_EXTERNAL_INTEGRATION="true" - if [ -n "$CIRCLE_PR_NUMBER" ]; then + if [ -n "$CIRCLE_PULL_REQUEST" ]; then TESTS_RUN_EXTERNAL_INTEGRATION="false" fi docker-compose run -e TESTS_RUN_EXTERNAL_INTEGRATION=$TESTS_RUN_EXTERNAL_INTEGRATION tests From 8a99fc0505c140ffd87355197a12d786b30f0478 Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Thu, 2 May 2019 13:39:13 +0200 Subject: [PATCH 003/243] Fix Azure Storage (#803) --- .../Configuration/AzureBlobStorageConfiguration.cs | 5 ++--- .../AzureBlobStorageConfigurationMetadata.cs | 11 +++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConfigurationMetadata.cs diff --git a/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConfiguration.cs b/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConfiguration.cs index 931e640a2..72cc828ae 100644 --- a/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConfiguration.cs +++ b/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConfiguration.cs @@ -1,9 +1,11 @@ using System.ComponentModel.DataAnnotations; using BTCPayServer.Storage.Services.Providers.Models; +using Microsoft.AspNetCore.Mvc; using TwentyTwenty.Storage.Azure; namespace BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration { + [ModelMetadataType(typeof(AzureBlobStorageConfigurationMetadata))] public class AzureBlobStorageConfiguration : AzureProviderOptions, IBaseStorageConfiguration { [Required] @@ -12,8 +14,5 @@ namespace BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration [RegularExpression(@"[a-z0-9-]+", ErrorMessage = "Characters must be lowercase or digits or -")] public string ContainerName { get; set; } - - [Required][AzureBlobStorageConnectionStringValidator] - public new string ConnectionString { get; set; } } } diff --git a/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConfigurationMetadata.cs b/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConfigurationMetadata.cs new file mode 100644 index 000000000..a16165488 --- /dev/null +++ b/BTCPayServer/Storage/Services/Providers/AzureBlobStorage/Configuration/AzureBlobStorageConfigurationMetadata.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration +{ + public class AzureBlobStorageConfigurationMetadata + { + [Required] + [AzureBlobStorageConnectionStringValidator] + public string ConnectionString { get; set; } + } +} From 87a4f02f18feeebbedeed58243fc5d8ae7fa28b4 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Thu, 2 May 2019 20:45:49 +0900 Subject: [PATCH 004/243] bump NBXplorer --- BTCPayServer.Tests/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index 6cc03a346..4507adc40 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -71,7 +71,7 @@ services: nbxplorer: - image: nicolasdorier/nbxplorer:2.0.0.34 + image: nicolasdorier/nbxplorer:2.0.0.35 restart: unless-stopped ports: - "32838:32838" From 19a990b0958dcb031213fae76db3469899eb612b Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Thu, 2 May 2019 14:01:08 +0200 Subject: [PATCH 005/243] Add U2f Login Support (#787) * init u2f * ux fixes * Cleanup Manage Controller * final changes * remove logs * remove console log * fix text for u2f * Use Is Secure instead of IsHttps * add some basic u2f tests * move loaders to before title * missing commit * refactor after nicolas wtf moment --- BTCPayServer.Tests/TestAccount.cs | 7 +- BTCPayServer.Tests/UnitTest1.cs | 74 ++ BTCPayServer/BTCPayServer.csproj | 3 + BTCPayServer/Controllers/AccountController.cs | 132 ++- .../Controllers/ManageController.2FA.cs | 205 +++++ .../Controllers/ManageController.U2F.cs | 87 ++ BTCPayServer/Controllers/ManageController.cs | 200 +---- BTCPayServer/Data/ApplicationDbContext.cs | 5 + BTCPayServer/Hosting/BTCPayServerServices.cs | 2 + .../20190425081749_AddU2fDevices.Designer.cs | 667 ++++++++++++++++ .../20190425081749_AddU2fDevices.cs | 54 ++ .../ApplicationDbContextModelSnapshot.cs | 36 +- .../SecondaryLoginViewModel.cs | 10 + BTCPayServer/Models/ApplicationUser.cs | 3 + .../U2F/Models/AddU2FDeviceViewModel.cs | 12 + .../U2F/Models/LoginWithU2FViewModel.cs | 28 + BTCPayServer/U2F/Models/ServerChallenge.cs | 10 + .../U2F/Models/ServerRegisterResponse.cs | 9 + .../U2F/Models/U2FAuthenticationViewModel.cs | 10 + BTCPayServer/U2F/Models/U2FDevice.cs | 24 + .../Models/U2FDeviceAuthenticationRequest.cs | 15 + BTCPayServer/U2F/U2FService.cs | 218 +++++ .../Views/Account/LoginWith2fa.cshtml | 53 +- .../Account/LoginWithRecoveryCode.cshtml | 45 +- .../Views/Account/LoginWithU2F.cshtml | 53 ++ .../Views/Account/SecondaryLogin.cshtml | 47 ++ BTCPayServer/Views/Manage/AddU2FDevice.cshtml | 58 ++ BTCPayServer/Views/Manage/ManageNavPages.cs | 2 +- .../Views/Manage/U2FAuthentication.cshtml | 47 ++ BTCPayServer/Views/Manage/_Nav.cshtml | 1 + .../wwwroot/vendor/u2f/u2f-api-1.1.js | 749 ++++++++++++++++++ 31 files changed, 2613 insertions(+), 253 deletions(-) create mode 100644 BTCPayServer/Controllers/ManageController.2FA.cs create mode 100644 BTCPayServer/Controllers/ManageController.U2F.cs create mode 100644 BTCPayServer/Migrations/20190425081749_AddU2fDevices.Designer.cs create mode 100644 BTCPayServer/Migrations/20190425081749_AddU2fDevices.cs create mode 100644 BTCPayServer/Models/AccountViewModels/SecondaryLoginViewModel.cs create mode 100644 BTCPayServer/U2F/Models/AddU2FDeviceViewModel.cs create mode 100644 BTCPayServer/U2F/Models/LoginWithU2FViewModel.cs create mode 100644 BTCPayServer/U2F/Models/ServerChallenge.cs create mode 100644 BTCPayServer/U2F/Models/ServerRegisterResponse.cs create mode 100644 BTCPayServer/U2F/Models/U2FAuthenticationViewModel.cs create mode 100644 BTCPayServer/U2F/Models/U2FDevice.cs create mode 100644 BTCPayServer/U2F/Models/U2FDeviceAuthenticationRequest.cs create mode 100644 BTCPayServer/U2F/U2FService.cs create mode 100644 BTCPayServer/Views/Account/LoginWithU2F.cshtml create mode 100644 BTCPayServer/Views/Account/SecondaryLogin.cshtml create mode 100644 BTCPayServer/Views/Manage/AddU2FDevice.cshtml create mode 100644 BTCPayServer/Views/Manage/U2FAuthentication.cshtml create mode 100644 BTCPayServer/wwwroot/vendor/u2f/u2f-api-1.1.js diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index 5ef6b0b08..88647077c 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -113,15 +113,18 @@ namespace BTCPayServer.Tests private async Task RegisterAsync() { var account = parent.PayTester.GetController(); - await account.Register(new RegisterViewModel() + RegisterDetails = new RegisterViewModel() { Email = Guid.NewGuid() + "@toto.com", ConfirmPassword = "Kitten0@", Password = "Kitten0@", - }); + }; + await account.Register(RegisterDetails); UserId = account.RegisteredUserId; } + public RegisterViewModel RegisterDetails{ get; set; } + public Bitpay BitPay { get; set; diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 3bf6ee678..b11a366e7 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -56,6 +56,8 @@ using BTCPayServer.Configuration; using System.Security; using System.Runtime.CompilerServices; using System.Net; +using BTCPayServer.Models.AccountViewModels; +using BTCPayServer.Services.U2F.Models; namespace BTCPayServer.Tests { @@ -2608,6 +2610,78 @@ donation: } + + [Fact] + [Trait("Integration", "Integration")] + public async Task CanLoginWithNoSecondaryAuthSystemsOrRequestItWhenAdded() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + + var accountController = tester.PayTester.GetController(); + + //no 2fa or u2f enabled, login should work + Assert.Equal(nameof(HomeController.Index), Assert.IsType(await accountController.Login(new LoginViewModel() + { + Email = user.RegisterDetails.Email, + Password = user.RegisterDetails.Password + })).ActionName); + + var manageController = user.GetController(); + + //by default no u2f devices available + Assert.Empty(Assert.IsType(Assert.IsType(await manageController.U2FAuthentication()).Model).Devices); + var addRequest = Assert.IsType(Assert.IsType(manageController.AddU2FDevice("label")).Model); + //name should match the one provided in beginning + Assert.Equal("label",addRequest.Name); + + //sending an invalid response model back to server, should error out + var statusMessage = Assert + .IsType(await manageController.AddU2FDevice(addRequest)) + .RouteValues["StatusMessage"].ToString(); + Assert.NotNull(statusMessage); + Assert.Equal(StatusMessageModel.StatusSeverity.Error, new StatusMessageModel(statusMessage).Severity); + + var contextFactory = tester.PayTester.GetService(); + + //add a fake u2f device in db directly since emulating a u2f device is hard and annoying + using (var context = contextFactory.CreateContext()) + { + var newDevice = new U2FDevice() + { + Name = "fake", + Counter = 0, + KeyHandle = UTF8Encoding.UTF8.GetBytes("fake"), + PublicKey= UTF8Encoding.UTF8.GetBytes("fake"), + AttestationCert= UTF8Encoding.UTF8.GetBytes("fake"), + ApplicationUserId= user.UserId + }; + await context.U2FDevices.AddAsync(newDevice); + await context.SaveChangesAsync(); + + Assert.NotNull(newDevice.Id); + Assert.NotEmpty(Assert.IsType(Assert.IsType(await manageController.U2FAuthentication()).Model).Devices); + + } + + //check if we are showing the u2f login screen now + var secondLoginResult = Assert.IsType(await accountController.Login(new LoginViewModel() + { + Email = user.RegisterDetails.Email, + Password = user.RegisterDetails.Password + })); + + Assert.Equal("SecondaryLogin", secondLoginResult.ViewName); + var vm = Assert.IsType(secondLoginResult.Model); + //2fa was never enabled for user so this should be empty + Assert.Null(vm.LoginWith2FaViewModel); + Assert.NotNull(vm.LoginWithU2FViewModel); + } + } + private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx) { var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString(); diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 78f17aa48..7889bdcb4 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -74,6 +74,7 @@ + @@ -129,9 +130,11 @@ + + diff --git a/BTCPayServer/Controllers/AccountController.cs b/BTCPayServer/Controllers/AccountController.cs index 94cc11c42..53d730c97 100644 --- a/BTCPayServer/Controllers/AccountController.cs +++ b/BTCPayServer/Controllers/AccountController.cs @@ -18,6 +18,9 @@ using BTCPayServer.Services.Stores; using BTCPayServer.Logging; using BTCPayServer.Security; using System.Globalization; +using BTCPayServer.Services.U2F; +using BTCPayServer.Services.U2F.Models; +using Newtonsoft.Json; using NicolasDorier.RateLimits; namespace BTCPayServer.Controllers @@ -33,6 +36,8 @@ namespace BTCPayServer.Controllers RoleManager _RoleManager; SettingsRepository _SettingsRepository; Configuration.BTCPayServerOptions _Options; + private readonly BTCPayServerEnvironment _btcPayServerEnvironment; + private readonly U2FService _u2FService; ILogger _logger; public AccountController( @@ -42,7 +47,9 @@ namespace BTCPayServer.Controllers SignInManager signInManager, EmailSenderFactory emailSenderFactory, SettingsRepository settingsRepository, - Configuration.BTCPayServerOptions options) + Configuration.BTCPayServerOptions options, + BTCPayServerEnvironment btcPayServerEnvironment, + U2FService u2FService) { this.storeRepository = storeRepository; _userManager = userManager; @@ -51,6 +58,8 @@ namespace BTCPayServer.Controllers _RoleManager = roleManager; _SettingsRepository = settingsRepository; _Options = options; + _btcPayServerEnvironment = btcPayServerEnvironment; + _u2FService = u2FService; _logger = Logs.PayServer; } @@ -91,8 +100,39 @@ namespace BTCPayServer.Controllers return View(model); } } - // This doesn't count login failures towards account lockout - // To enable password failures to trigger account lockout, set lockoutOnFailure: true + + if (!await _userManager.IsLockedOutAsync(user) && await _u2FService.HasDevices(user.Id)) + { + if (await _userManager.CheckPasswordAsync(user, model.Password)) + { + LoginWith2faViewModel twoFModel = null; + + if (user.TwoFactorEnabled) + { + // we need to do an actual sign in attempt so that 2fa can function in next step + await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true); + twoFModel = new LoginWith2faViewModel + { + RememberMe = model.RememberMe + }; + } + + return View("SecondaryLogin", new SecondaryLoginViewModel() + { + LoginWith2FaViewModel = twoFModel, + LoginWithU2FViewModel = await BuildU2FViewModel(model.RememberMe, user) + }); + } + else + { + var incrementAccessFailedResult = await _userManager.AccessFailedAsync(user); + ModelState.AddModelError(string.Empty, "Invalid login attempt."); + return View(model); + + } + } + + var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true); if (result.Succeeded) { @@ -101,10 +141,12 @@ namespace BTCPayServer.Controllers } if (result.RequiresTwoFactor) { - return RedirectToAction(nameof(LoginWith2fa), new + return View("SecondaryLogin", new SecondaryLoginViewModel() { - returnUrl, - model.RememberMe + LoginWith2FaViewModel = new LoginWith2faViewModel() + { + RememberMe = model.RememberMe + } }); } if (result.IsLockedOut) @@ -123,6 +165,71 @@ namespace BTCPayServer.Controllers return View(model); } + private async Task BuildU2FViewModel(bool rememberMe, ApplicationUser user) + { + if (_btcPayServerEnvironment.IsSecure) + { + var u2fChallenge = await _u2FService.GenerateDeviceChallenges(user.Id, + Request.GetAbsoluteUriNoPathBase().ToString().TrimEnd('/')); + + return new LoginWithU2FViewModel() + { + Version = u2fChallenge[0].version, + Challenge = u2fChallenge[0].challenge, + Challenges = JsonConvert.SerializeObject(u2fChallenge), + AppId = u2fChallenge[0].appId, + UserId = user.Id, + RememberMe = rememberMe + }; + } + + return null; + } + + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task LoginWithU2F(LoginWithU2FViewModel viewModel, string returnUrl = null) + { + ViewData["ReturnUrl"] = returnUrl; + var user = await _userManager.FindByIdAsync(viewModel.UserId); + + if (user == null) + { + return NotFound(); + } + + var errorMessage = string.Empty; + try + { + if (await _u2FService.AuthenticateUser(viewModel.UserId, viewModel.DeviceResponse)) + { + await _signInManager.SignInAsync(user, viewModel.RememberMe, "U2F"); + _logger.LogInformation("User logged in."); + return RedirectToLocal(returnUrl); + } + + errorMessage = "Invalid login attempt."; + } + catch (Exception e) + { + + errorMessage = e.Message; + } + + ModelState.AddModelError(string.Empty, errorMessage); + return View("SecondaryLogin", new SecondaryLoginViewModel() + { + LoginWithU2FViewModel = viewModel, + LoginWith2FaViewModel = !user.TwoFactorEnabled + ? null + : new LoginWith2faViewModel() + { + RememberMe = viewModel.RememberMe + } + }); + } + [HttpGet] [AllowAnonymous] public async Task LoginWith2fa(bool rememberMe, string returnUrl = null) @@ -135,10 +242,13 @@ namespace BTCPayServer.Controllers throw new ApplicationException($"Unable to load two-factor authentication user."); } - var model = new LoginWith2faViewModel { RememberMe = rememberMe }; ViewData["ReturnUrl"] = returnUrl; - return View(model); + return View("SecondaryLogin", new SecondaryLoginViewModel() + { + LoginWith2FaViewModel = new LoginWith2faViewModel { RememberMe = rememberMe }, + LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id))? await BuildU2FViewModel(rememberMe, user): null + }); } [HttpPost] @@ -175,7 +285,11 @@ namespace BTCPayServer.Controllers { _logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id); ModelState.AddModelError(string.Empty, "Invalid authenticator code."); - return View(); + return View("SecondaryLogin", new SecondaryLoginViewModel() + { + LoginWith2FaViewModel = model, + LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id))? await BuildU2FViewModel(rememberMe, user): null + }); } } diff --git a/BTCPayServer/Controllers/ManageController.2FA.cs b/BTCPayServer/Controllers/ManageController.2FA.cs new file mode 100644 index 000000000..effc013ee --- /dev/null +++ b/BTCPayServer/Controllers/ManageController.2FA.cs @@ -0,0 +1,205 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Models.ManageViewModels; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace BTCPayServer.Controllers +{ + public partial class ManageController + { + private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; + + [HttpGet] + public async Task TwoFactorAuthentication() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var model = new TwoFactorAuthenticationViewModel + { + HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null, + Is2faEnabled = user.TwoFactorEnabled, + RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user), + }; + + return View(model); + } + + [HttpGet] + public async Task Disable2faWarning() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!user.TwoFactorEnabled) + { + throw new ApplicationException( + $"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'."); + } + + return View(nameof(Disable2fa)); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Disable2fa() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false); + if (!disable2faResult.Succeeded) + { + throw new ApplicationException( + $"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'."); + } + + _logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id); + return RedirectToAction(nameof(TwoFactorAuthentication)); + } + + [HttpGet] + public async Task EnableAuthenticator() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); + if (string.IsNullOrEmpty(unformattedKey)) + { + await _userManager.ResetAuthenticatorKeyAsync(user); + unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); + } + + var model = new EnableAuthenticatorViewModel + { + SharedKey = FormatKey(unformattedKey), + AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey) + }; + + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task EnableAuthenticator(EnableAuthenticatorViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + // Strip spaces and hypens + var verificationCode = model.Code.Replace(" ", string.Empty, StringComparison.InvariantCulture) + .Replace("-", string.Empty, StringComparison.InvariantCulture); + + var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync( + user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); + + if (!is2faTokenValid) + { + ModelState.AddModelError(nameof(model.Code), "Verification code is invalid."); + return View(model); + } + + await _userManager.SetTwoFactorEnabledAsync(user, true); + _logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id); + return RedirectToAction(nameof(GenerateRecoveryCodes)); + } + + [HttpGet] + public IActionResult ResetAuthenticatorWarning() + { + return View(nameof(ResetAuthenticator)); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task ResetAuthenticator() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + await _userManager.SetTwoFactorEnabledAsync(user, false); + await _userManager.ResetAuthenticatorKeyAsync(user); + _logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id); + + return RedirectToAction(nameof(EnableAuthenticator)); + } + + [HttpGet] + public async Task GenerateRecoveryCodes() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!user.TwoFactorEnabled) + { + throw new ApplicationException( + $"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled."); + } + + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + var model = new GenerateRecoveryCodesViewModel {RecoveryCodes = recoveryCodes.ToArray()}; + + _logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id); + + return View(model); + } + + private string GenerateQrCodeUri(string email, string unformattedKey) + { + return string.Format(CultureInfo.InvariantCulture, + AuthenicatorUriFormat, + _urlEncoder.Encode("BTCPayServer"), + _urlEncoder.Encode(email), + unformattedKey); + } + + private string FormatKey(string unformattedKey) + { + var result = new StringBuilder(); + int currentPosition = 0; + while (currentPosition + 4 < unformattedKey.Length) + { + result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" "); + currentPosition += 4; + } + + if (currentPosition < unformattedKey.Length) + { + result.Append(unformattedKey.Substring(currentPosition)); + } + + return result.ToString().ToLowerInvariant(); + } + } +} diff --git a/BTCPayServer/Controllers/ManageController.U2F.cs b/BTCPayServer/Controllers/ManageController.U2F.cs new file mode 100644 index 000000000..966767bf0 --- /dev/null +++ b/BTCPayServer/Controllers/ManageController.U2F.cs @@ -0,0 +1,87 @@ +using System; +using System.Threading.Tasks; +using BTCPayServer.Models; +using BTCPayServer.Services.U2F.Models; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Controllers +{ + public partial class ManageController + { + [HttpGet] + public async Task U2FAuthentication(string statusMessage = null) + { + return View(new U2FAuthenticationViewModel() + { + StatusMessage = statusMessage, + Devices = await _u2FService.GetDevices(_userManager.GetUserId(User)) + }); + } + + [HttpGet] + public async Task RemoveU2FDevice(string id) + { + await _u2FService.RemoveDevice(id, _userManager.GetUserId(User)); + return RedirectToAction("U2FAuthentication", new + { + StatusMessage = "Device removed" + }); + } + + [HttpGet] + public IActionResult AddU2FDevice(string name) + { + if (!_btcPayServerEnvironment.IsSecure) + { + return RedirectToAction("U2FAuthentication", new + { + StatusMessage = new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Error, + Message = "Cannot register U2F device while not on https or tor" + } + }); + } + + var serverRegisterResponse = _u2FService.StartDeviceRegistration(_userManager.GetUserId(User), + Request.GetAbsoluteUriNoPathBase().ToString().TrimEnd('/')); + + return View(new AddU2FDeviceViewModel() + { + AppId = serverRegisterResponse.AppId, + Challenge = serverRegisterResponse.Challenge, + Version = serverRegisterResponse.Version, + Name = name + }); + } + + [HttpPost] + public async Task AddU2FDevice(AddU2FDeviceViewModel viewModel) + { + try + { + if (await _u2FService.CompleteRegistration(_userManager.GetUserId(User), viewModel.DeviceResponse, + string.IsNullOrEmpty(viewModel.Name) ? "Unlabelled U2F Device" : viewModel.Name)) + { + return RedirectToAction("U2FAuthentication", new + { + StatusMessage = "Device added!" + }); + } + + throw new Exception("Could not add device."); + } + catch (Exception e) + { + return RedirectToAction("U2FAuthentication", new + { + StatusMessage = new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Error, + Message = e.Message + } + }); + } + } + } +} diff --git a/BTCPayServer/Controllers/ManageController.cs b/BTCPayServer/Controllers/ManageController.cs index 681f5c8df..ee69eee44 100644 --- a/BTCPayServer/Controllers/ManageController.cs +++ b/BTCPayServer/Controllers/ManageController.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.Encodings.Web; @@ -9,25 +8,23 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using BTCPayServer.Models; using BTCPayServer.Models.ManageViewModels; using BTCPayServer.Services; using BTCPayServer.Authentication; using Microsoft.AspNetCore.Hosting; -using NBitpayClient; -using NBitcoin; using BTCPayServer.Services.Stores; using BTCPayServer.Services.Wallets; using BTCPayServer.Services.Mails; using System.Globalization; using BTCPayServer.Security; +using BTCPayServer.Services.U2F; namespace BTCPayServer.Controllers { [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] [Route("[controller]/[action]")] - public class ManageController : Controller + public partial class ManageController : Controller { private readonly UserManager _userManager; private readonly SignInManager _signInManager; @@ -36,10 +33,11 @@ namespace BTCPayServer.Controllers private readonly UrlEncoder _urlEncoder; TokenRepository _TokenRepository; IHostingEnvironment _Env; + private readonly U2FService _u2FService; + private readonly BTCPayServerEnvironment _btcPayServerEnvironment; StoreRepository _StoreRepository; - private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; public ManageController( UserManager userManager, @@ -50,7 +48,9 @@ namespace BTCPayServer.Controllers TokenRepository tokenRepository, BTCPayWalletProvider walletProvider, StoreRepository storeRepository, - IHostingEnvironment env) + IHostingEnvironment env, + U2FService u2FService, + BTCPayServerEnvironment btcPayServerEnvironment) { _userManager = userManager; _signInManager = signInManager; @@ -59,6 +59,8 @@ namespace BTCPayServer.Controllers _urlEncoder = urlEncoder; _TokenRepository = tokenRepository; _Env = env; + _u2FService = u2FService; + _btcPayServerEnvironment = btcPayServerEnvironment; _StoreRepository = storeRepository; } @@ -339,163 +341,6 @@ namespace BTCPayServer.Controllers return RedirectToAction(nameof(ExternalLogins)); } - [HttpGet] - public async Task TwoFactorAuthentication() - { - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); - } - - var model = new TwoFactorAuthenticationViewModel - { - HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null, - Is2faEnabled = user.TwoFactorEnabled, - RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user), - }; - - return View(model); - } - - [HttpGet] - public async Task Disable2faWarning() - { - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); - } - - if (!user.TwoFactorEnabled) - { - throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'."); - } - - return View(nameof(Disable2fa)); - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Disable2fa() - { - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); - } - - var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false); - if (!disable2faResult.Succeeded) - { - throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'."); - } - - _logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id); - return RedirectToAction(nameof(TwoFactorAuthentication)); - } - - [HttpGet] - public async Task EnableAuthenticator() - { - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); - } - - var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); - if (string.IsNullOrEmpty(unformattedKey)) - { - await _userManager.ResetAuthenticatorKeyAsync(user); - unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); - } - - var model = new EnableAuthenticatorViewModel - { - SharedKey = FormatKey(unformattedKey), - AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey) - }; - - return View(model); - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task EnableAuthenticator(EnableAuthenticatorViewModel model) - { - if (!ModelState.IsValid) - { - return View(model); - } - - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); - } - - // Strip spaces and hypens - var verificationCode = model.Code.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture); - - var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync( - user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); - - if (!is2faTokenValid) - { - ModelState.AddModelError(nameof(model.Code), "Verification code is invalid."); - return View(model); - } - - await _userManager.SetTwoFactorEnabledAsync(user, true); - _logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id); - return RedirectToAction(nameof(GenerateRecoveryCodes)); - } - - [HttpGet] - public IActionResult ResetAuthenticatorWarning() - { - return View(nameof(ResetAuthenticator)); - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task ResetAuthenticator() - { - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); - } - - await _userManager.SetTwoFactorEnabledAsync(user, false); - await _userManager.ResetAuthenticatorKeyAsync(user); - _logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id); - - return RedirectToAction(nameof(EnableAuthenticator)); - } - - [HttpGet] - public async Task GenerateRecoveryCodes() - { - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); - } - - if (!user.TwoFactorEnabled) - { - throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled."); - } - - var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); - var model = new GenerateRecoveryCodesViewModel { RecoveryCodes = recoveryCodes.ToArray() }; - - _logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id); - - return View(model); - } #region Helpers @@ -506,33 +351,6 @@ namespace BTCPayServer.Controllers ModelState.AddModelError(string.Empty, error.Description); } } - - private string FormatKey(string unformattedKey) - { - var result = new StringBuilder(); - int currentPosition = 0; - while (currentPosition + 4 < unformattedKey.Length) - { - result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" "); - currentPosition += 4; - } - if (currentPosition < unformattedKey.Length) - { - result.Append(unformattedKey.Substring(currentPosition)); - } - - return result.ToString().ToLowerInvariant(); - } - - private string GenerateQrCodeUri(string email, string unformattedKey) - { - return string.Format(CultureInfo.InvariantCulture, - AuthenicatorUriFormat, - _urlEncoder.Encode("BTCPayServer"), - _urlEncoder.Encode(email), - unformattedKey); - } - #endregion } } diff --git a/BTCPayServer/Data/ApplicationDbContext.cs b/BTCPayServer/Data/ApplicationDbContext.cs index 169129606..473de8f8d 100644 --- a/BTCPayServer/Data/ApplicationDbContext.cs +++ b/BTCPayServer/Data/ApplicationDbContext.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using BTCPayServer.Models; using BTCPayServer.Services.PaymentRequests; +using BTCPayServer.Services.U2F.Models; using BTCPayServer.Storage.Models; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -98,7 +99,10 @@ namespace BTCPayServer.Data { get; set; } + + public DbSet U2FDevices { get; set; } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { var isConfigured = optionsBuilder.Options.Extensions.OfType().Any(); @@ -223,4 +227,5 @@ namespace BTCPayServer.Data .HasIndex(o => o.Status); } } + } diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 419f6dbb6..3341972b8 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -47,6 +47,7 @@ using NBXplorer.DerivationStrategy; using NicolasDorier.RateLimits; using Npgsql; using BTCPayServer.Services.Apps; +using BTCPayServer.Services.U2F; using BundlerMinifier.TagHelpers; namespace BTCPayServer.Hosting @@ -80,6 +81,7 @@ namespace BTCPayServer.Hosting services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(o => { diff --git a/BTCPayServer/Migrations/20190425081749_AddU2fDevices.Designer.cs b/BTCPayServer/Migrations/20190425081749_AddU2fDevices.Designer.cs new file mode 100644 index 000000000..07894522f --- /dev/null +++ b/BTCPayServer/Migrations/20190425081749_AddU2fDevices.Designer.cs @@ -0,0 +1,667 @@ +// +using System; +using BTCPayServer.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace BTCPayServer.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20190425081749_AddU2fDevices")] + partial class AddU2fDevices + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.8-servicing-32085"); + + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.Property("Address") + .ValueGeneratedOnAdd(); + + b.Property("CreatedTime"); + + b.Property("InvoiceDataId"); + + b.HasKey("Address"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("AddressInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(50); + + b.Property("StoreId") + .HasMaxLength(50); + + b.HasKey("Id"); + + b.HasIndex("StoreId"); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("BTCPayServer.Data.AppData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AppType"); + + b.Property("Created"); + + b.Property("Name"); + + b.Property("Settings"); + + b.Property("StoreDataId"); + + b.Property("TagAllInvoices"); + + b.HasKey("Id"); + + b.HasIndex("StoreDataId"); + + b.ToTable("Apps"); + }); + + modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => + { + b.Property("InvoiceDataId"); + + b.Property("Address"); + + b.Property("Assigned"); + + b.Property("CryptoCode"); + + b.Property("UnAssigned"); + + b.HasKey("InvoiceDataId", "Address"); + + b.ToTable("HistoricalAddressInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Blob"); + + b.Property("Created"); + + b.Property("CustomerEmail"); + + b.Property("ExceptionStatus"); + + b.Property("ItemCode"); + + b.Property("OrderId"); + + b.Property("Status"); + + b.Property("StoreDataId"); + + b.HasKey("Id"); + + b.HasIndex("StoreDataId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b => + { + b.Property("InvoiceDataId"); + + b.Property("UniqueId"); + + b.Property("Message"); + + b.Property("Timestamp"); + + b.HasKey("InvoiceDataId", "UniqueId"); + + b.ToTable("InvoiceEvents"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Label"); + + b.Property("PairingTime"); + + b.Property("SIN"); + + b.Property("StoreDataId"); + + b.HasKey("Id"); + + b.HasIndex("SIN"); + + b.HasIndex("StoreDataId"); + + b.ToTable("PairedSINData"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateCreated"); + + b.Property("Expiration"); + + b.Property("Facade"); + + b.Property("Label"); + + b.Property("SIN"); + + b.Property("StoreDataId"); + + b.Property("TokenValue"); + + b.HasKey("Id"); + + b.ToTable("PairingCodes"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Accounted"); + + b.Property("Blob"); + + b.Property("InvoiceDataId"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => + { + b.Property("Id"); + + b.HasKey("Id"); + + b.ToTable("PendingInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Blob"); + + b.Property("InvoiceDataId"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("RefundAddresses"); + }); + + modelBuilder.Entity("BTCPayServer.Data.SettingData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Value"); + + b.HasKey("Id"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("BTCPayServer.Data.StoreData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DefaultCrypto"); + + b.Property("DerivationStrategies"); + + b.Property("DerivationStrategy"); + + b.Property("SpeedPolicy"); + + b.Property("StoreBlob"); + + b.Property("StoreCertificate"); + + b.Property("StoreName"); + + b.Property("StoreWebsite"); + + b.HasKey("Id"); + + b.ToTable("Stores"); + }); + + modelBuilder.Entity("BTCPayServer.Data.UserStore", b => + { + b.Property("ApplicationUserId"); + + b.Property("StoreDataId"); + + b.Property("Role"); + + b.HasKey("ApplicationUserId", "StoreDataId"); + + b.HasIndex("StoreDataId"); + + b.ToTable("UserStore"); + }); + + modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("RequiresEmailConfirmation"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Blob"); + + b.Property("Status"); + + b.Property("StoreDataId"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("StoreDataId"); + + b.ToTable("PaymentRequests"); + }); + + modelBuilder.Entity("BTCPayServer.Services.U2F.Models.U2FDevice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApplicationUserId"); + + b.Property("AttestationCert") + .IsRequired(); + + b.Property("Counter"); + + b.Property("KeyHandle") + .IsRequired(); + + b.Property("Name"); + + b.Property("PublicKey") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("U2FDevices"); + }); + + modelBuilder.Entity("BTCPayServer.Storage.Models.StoredFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApplicationUserId"); + + b.Property("FileName"); + + b.Property("StorageFileName"); + + b.Property("Timestamp"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("AddressInvoices") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("APIKeys") + .HasForeignKey("StoreId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.AppData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("Apps") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("HistoricalAddressInvoices") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("Invoices") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("Events") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("PairedSINs") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("Payments") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("PendingInvoices") + .HasForeignKey("Id") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("RefundAddresses") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.UserStore", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser") + .WithMany("UserStores") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("UserStores") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("PaymentRequests") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Services.U2F.Models.U2FDevice", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser") + .WithMany("U2FDevices") + .HasForeignKey("ApplicationUserId"); + }); + + modelBuilder.Entity("BTCPayServer.Storage.Models.StoredFile", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser") + .WithMany("StoredFiles") + .HasForeignKey("ApplicationUserId"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BTCPayServer/Migrations/20190425081749_AddU2fDevices.cs b/BTCPayServer/Migrations/20190425081749_AddU2fDevices.cs new file mode 100644 index 000000000..4d5575697 --- /dev/null +++ b/BTCPayServer/Migrations/20190425081749_AddU2fDevices.cs @@ -0,0 +1,54 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace BTCPayServer.Migrations +{ + public partial class AddU2fDevices : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Facade", + table: "PairedSINData"); + + migrationBuilder.CreateTable( + name: "U2FDevices", + columns: table => new + { + Id = table.Column(nullable: false), + Name = table.Column(nullable: true), + KeyHandle = table.Column(nullable: false), + PublicKey = table.Column(nullable: false), + AttestationCert = table.Column(nullable: false), + Counter = table.Column(nullable: false), + ApplicationUserId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_U2FDevices", x => x.Id); + table.ForeignKey( + name: "FK_U2FDevices_AspNetUsers_ApplicationUserId", + column: x => x.ApplicationUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_U2FDevices_ApplicationUserId", + table: "U2FDevices", + column: "ApplicationUserId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "U2FDevices"); + + migrationBuilder.AddColumn( + name: "Facade", + table: "PairedSINData", + nullable: true); + } + } +} diff --git a/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs index 919b92944..4ba99f2fe 100644 --- a/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs @@ -137,8 +137,6 @@ namespace BTCPayServer.Migrations b.Property("Id") .ValueGeneratedOnAdd(); - b.Property("Facade"); - b.Property("Label"); b.Property("PairingTime"); @@ -348,6 +346,33 @@ namespace BTCPayServer.Migrations b.ToTable("PaymentRequests"); }); + modelBuilder.Entity("BTCPayServer.Services.U2F.Models.U2FDevice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApplicationUserId"); + + b.Property("AttestationCert") + .IsRequired(); + + b.Property("Counter"); + + b.Property("KeyHandle") + .IsRequired(); + + b.Property("Name"); + + b.Property("PublicKey") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("U2FDevices"); + }); + modelBuilder.Entity("BTCPayServer.Storage.Models.StoredFile", b => { b.Property("Id") @@ -576,6 +601,13 @@ namespace BTCPayServer.Migrations .OnDelete(DeleteBehavior.Cascade); }); + modelBuilder.Entity("BTCPayServer.Services.U2F.Models.U2FDevice", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser") + .WithMany("U2FDevices") + .HasForeignKey("ApplicationUserId"); + }); + modelBuilder.Entity("BTCPayServer.Storage.Models.StoredFile", b => { b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser") diff --git a/BTCPayServer/Models/AccountViewModels/SecondaryLoginViewModel.cs b/BTCPayServer/Models/AccountViewModels/SecondaryLoginViewModel.cs new file mode 100644 index 000000000..5ffaeecc3 --- /dev/null +++ b/BTCPayServer/Models/AccountViewModels/SecondaryLoginViewModel.cs @@ -0,0 +1,10 @@ +using BTCPayServer.Services.U2F.Models; + +namespace BTCPayServer.Models.AccountViewModels +{ + public class SecondaryLoginViewModel + { + public LoginWith2faViewModel LoginWith2FaViewModel { get; set; } + public LoginWithU2FViewModel LoginWithU2FViewModel { get; set; } + } +} \ No newline at end of file diff --git a/BTCPayServer/Models/ApplicationUser.cs b/BTCPayServer/Models/ApplicationUser.cs index 31360f52c..7721e2b1b 100644 --- a/BTCPayServer/Models/ApplicationUser.cs +++ b/BTCPayServer/Models/ApplicationUser.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using BTCPayServer.Data; +using BTCPayServer.Services.U2F.Models; using BTCPayServer.Storage.Models; namespace BTCPayServer.Models @@ -27,5 +28,7 @@ namespace BTCPayServer.Models get; set; } + + public List U2FDevices { get; set; } } } diff --git a/BTCPayServer/U2F/Models/AddU2FDeviceViewModel.cs b/BTCPayServer/U2F/Models/AddU2FDeviceViewModel.cs new file mode 100644 index 000000000..193507512 --- /dev/null +++ b/BTCPayServer/U2F/Models/AddU2FDeviceViewModel.cs @@ -0,0 +1,12 @@ +namespace BTCPayServer.Services.U2F.Models +{ + public class AddU2FDeviceViewModel + { + public string AppId{ get; set; } + public string Challenge { get; set; } + public string Version { get; set; } + public string DeviceResponse { get; set; } + + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/BTCPayServer/U2F/Models/LoginWithU2FViewModel.cs b/BTCPayServer/U2F/Models/LoginWithU2FViewModel.cs new file mode 100644 index 000000000..33daa2efe --- /dev/null +++ b/BTCPayServer/U2F/Models/LoginWithU2FViewModel.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; + +namespace BTCPayServer.Services.U2F.Models +{ + public class LoginWithU2FViewModel + { + public string UserId { get; set; } + [Required] + [Display(Name = "App id")] + public string AppId { get; set; } + + [Required] + [Display(Name = "Version")] + public string Version { get; set; } + + [Required] + [Display(Name = "Device Response")] + public string DeviceResponse { get; set; } + + [Display(Name = "Challenges")] + public string Challenges { get; set; } + + [Display(Name = "Challenge")] + public string Challenge { get; set; } + + public bool RememberMe { get; set; } + } +} diff --git a/BTCPayServer/U2F/Models/ServerChallenge.cs b/BTCPayServer/U2F/Models/ServerChallenge.cs new file mode 100644 index 000000000..6c72324e1 --- /dev/null +++ b/BTCPayServer/U2F/Models/ServerChallenge.cs @@ -0,0 +1,10 @@ +namespace BTCPayServer.Services.U2F.Models +{ + public class ServerChallenge + { + public string challenge { get; set; } + public string version { get; set; } + public string appId { get; set; } + public string keyHandle { get; set; } + } +} \ No newline at end of file diff --git a/BTCPayServer/U2F/Models/ServerRegisterResponse.cs b/BTCPayServer/U2F/Models/ServerRegisterResponse.cs new file mode 100644 index 000000000..aaf7716b9 --- /dev/null +++ b/BTCPayServer/U2F/Models/ServerRegisterResponse.cs @@ -0,0 +1,9 @@ +namespace BTCPayServer.Services.U2F.Models +{ + public class ServerRegisterResponse + { + public string AppId { get; set; } + public string Challenge { get; set; } + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/BTCPayServer/U2F/Models/U2FAuthenticationViewModel.cs b/BTCPayServer/U2F/Models/U2FAuthenticationViewModel.cs new file mode 100644 index 000000000..34e8e5cc7 --- /dev/null +++ b/BTCPayServer/U2F/Models/U2FAuthenticationViewModel.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace BTCPayServer.Services.U2F.Models +{ + public class U2FAuthenticationViewModel + { + public string StatusMessage { get; set; } + public List Devices { get; set; } + } +} diff --git a/BTCPayServer/U2F/Models/U2FDevice.cs b/BTCPayServer/U2F/Models/U2FDevice.cs new file mode 100644 index 000000000..5debe133a --- /dev/null +++ b/BTCPayServer/U2F/Models/U2FDevice.cs @@ -0,0 +1,24 @@ +using System; +using System.ComponentModel.DataAnnotations; +using BTCPayServer.Models; + +namespace BTCPayServer.Services.U2F.Models +{ + public class U2FDevice + { + public string Id { get; set; } + + public string Name { get; set; } + + [Required] public byte[] KeyHandle { get; set; } + + [Required] public byte[] PublicKey { get; set; } + + [Required] public byte[] AttestationCert { get; set; } + + [Required] public int Counter { get; set; } + + public string ApplicationUserId { get; set; } + public ApplicationUser ApplicationUser { get; set; } + } +} diff --git a/BTCPayServer/U2F/Models/U2FDeviceAuthenticationRequest.cs b/BTCPayServer/U2F/Models/U2FDeviceAuthenticationRequest.cs new file mode 100644 index 000000000..d4679f403 --- /dev/null +++ b/BTCPayServer/U2F/Models/U2FDeviceAuthenticationRequest.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace BTCPayServer.Services.U2F.Models +{ + public class U2FDeviceAuthenticationRequest + { + public string KeyHandle { get; set; } + + [Required] public string Challenge { get; set; } + + [Required] [StringLength(200)] public string AppId { get; set; } + + [Required] [StringLength(50)] public string Version { get; set; } + } +} \ No newline at end of file diff --git a/BTCPayServer/U2F/U2FService.cs b/BTCPayServer/U2F/U2FService.cs new file mode 100644 index 000000000..4b0c90f3f --- /dev/null +++ b/BTCPayServer/U2F/U2FService.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Models; +using BTCPayServer.Services.U2F.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using NBitcoin; +using U2F.Core.Models; +using U2F.Core.Utils; + +namespace BTCPayServer.Services.U2F +{ + public class U2FService + { + private readonly ApplicationDbContextFactory _contextFactory; + + private ConcurrentDictionary> UserAuthenticationRequests + { + get; + set; + } + = new ConcurrentDictionary>(); + + public U2FService(ApplicationDbContextFactory contextFactory) + { + _contextFactory = contextFactory; + } + + public async Task> GetDevices(string userId) + { + using (var context = _contextFactory.CreateContext()) + { + return await context.U2FDevices + .Where(device => device.ApplicationUserId.Equals(userId, StringComparison.InvariantCulture)) + .ToListAsync(); + } + } + + public async Task RemoveDevice(string id, string userId) + { + using (var context = _contextFactory.CreateContext()) + { + var device = await context.U2FDevices.FindAsync(id); + if (device == null || !device.ApplicationUserId.Equals(userId, StringComparison.InvariantCulture)) + { + return; + } + + context.U2FDevices.Remove(device); + await context.SaveChangesAsync(); + } + } + + public async Task HasDevices(string userId) + { + using (var context = _contextFactory.CreateContext()) + { + return await context.U2FDevices.AnyAsync(fDevice => fDevice.ApplicationUserId.Equals(userId, StringComparison.InvariantCulture)); + } + } + + + public ServerRegisterResponse StartDeviceRegistration(string userId, string appId) + { + var startedRegistration = global::U2F.Core.Crypto.U2F.StartRegistration(appId); + + UserAuthenticationRequests.AddOrReplace(userId, new List() + { + new U2FDeviceAuthenticationRequest() + { + AppId = startedRegistration.AppId, + Challenge = startedRegistration.Challenge, + Version = global::U2F.Core.Crypto.U2F.U2FVersion, + } + }); + + return new ServerRegisterResponse + { + AppId = startedRegistration.AppId, + Challenge = startedRegistration.Challenge, + Version = startedRegistration.Version + }; + } + + public async Task CompleteRegistration(string userId, string deviceResponse, string name) + { + if (string.IsNullOrWhiteSpace(deviceResponse)) + return false; + + if (!UserAuthenticationRequests.ContainsKey(userId) || !UserAuthenticationRequests[userId].Any()) + { + return false; + } + + var registerResponse = RegisterResponse.FromJson(deviceResponse); + + //There is only 1 request when registering device + var authenticationRequest = UserAuthenticationRequests[userId].First(); + + var startedRegistration = + new StartedRegistration(authenticationRequest.Challenge, authenticationRequest.AppId); + var registration = global::U2F.Core.Crypto.U2F.FinishRegistration(startedRegistration, registerResponse); + + UserAuthenticationRequests.AddOrReplace(userId, new List()); + using (var context = _contextFactory.CreateContext()) + { + var duplicate = context.U2FDevices.Any(device => + device.ApplicationUserId.Equals(userId, StringComparison.InvariantCulture) && + device.KeyHandle.Equals(registration.KeyHandle) && + device.PublicKey.Equals(registration.PublicKey)); + + if (duplicate) + { + throw new InvalidOperationException("The U2F Device has already been registered with this user"); + } + + await context.U2FDevices.AddAsync(new U2FDevice() + { + AttestationCert = registration.AttestationCert, + Counter = Convert.ToInt32(registration.Counter), + Name = name, + KeyHandle = registration.KeyHandle, + PublicKey = registration.PublicKey, + ApplicationUserId = userId + }); + + await context.SaveChangesAsync(); + } + + return true; + } + + public async Task AuthenticateUser(string userId, string deviceResponse) + { + if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(deviceResponse)) + return false; + + var authenticateResponse = + AuthenticateResponse.FromJson(deviceResponse); + + using (var context = _contextFactory.CreateContext()) + { + var device = await context.U2FDevices.SingleOrDefaultAsync(fDevice => + fDevice.ApplicationUserId.Equals(userId, StringComparison.InvariantCulture) && + fDevice.KeyHandle.SequenceEqual(authenticateResponse.KeyHandle.Base64StringToByteArray())); + + if (device == null) + return false; + + // User will have a authentication request for each device they have registered so get the one that matches the device key handle + + var authenticationRequest = + UserAuthenticationRequests[userId].First(f => + f.KeyHandle.Equals(authenticateResponse.KeyHandle, StringComparison.InvariantCulture)); + var registration = new DeviceRegistration(device.KeyHandle, device.PublicKey, + device.AttestationCert, Convert.ToUInt32(device.Counter)); + + var authentication = new StartedAuthentication(authenticationRequest.Challenge, + authenticationRequest.AppId, authenticationRequest.KeyHandle); + + global::U2F.Core.Crypto.U2F.FinishAuthentication(authentication, authenticateResponse, registration); + + + UserAuthenticationRequests.AddOrReplace(userId, new List()); + + device.Counter = Convert.ToInt32(registration.Counter); + await context.SaveChangesAsync(); + } + + return true; + } + + public async Task> GenerateDeviceChallenges(string userId, string appId) + { + using (var context = _contextFactory.CreateContext()) + { + var devices = await context.U2FDevices.Where(fDevice => + fDevice.ApplicationUserId.Equals(userId, StringComparison.InvariantCulture)).ToListAsync(); + + if (devices.Count == 0) + return null; + + var requests = new List(); + + + var challenge = global::U2F.Core.Crypto.U2F.GenerateChallenge(); + + var serverChallenges = new List(); + foreach (var registeredDevice in devices) + { + serverChallenges.Add(new ServerChallenge + { + appId = appId, + challenge = challenge, + keyHandle = registeredDevice.KeyHandle.ByteArrayToBase64String(), + version = global::U2F.Core.Crypto.U2F.U2FVersion, + }); + + requests.Add( + new U2FDeviceAuthenticationRequest() + { + AppId = appId, + Challenge = challenge, + KeyHandle = registeredDevice.KeyHandle.ByteArrayToBase64String(), + Version = global::U2F.Core.Crypto.U2F.U2FVersion + }); + } + + UserAuthenticationRequests.AddOrReplace(userId, requests); + return serverChallenges; + } + } + } +} diff --git a/BTCPayServer/Views/Account/LoginWith2fa.cshtml b/BTCPayServer/Views/Account/LoginWith2fa.cshtml index e5ce75168..82b9e069f 100644 --- a/BTCPayServer/Views/Account/LoginWith2fa.cshtml +++ b/BTCPayServer/Views/Account/LoginWith2fa.cshtml @@ -1,45 +1,37 @@ @model LoginWith2faViewModel -@{ - ViewData["Title"] = "Two-factor authentication"; -}
-
+
-

@ViewData["Title"]

+

Two-factor authentication


-
-
-
- -
-
- - - + + +
+ + + +
+
+
+
-
-
- -
-
-
- -
- -
-
+
+
+ +
+ +
-
-
+

Don't have access to your authenticator device? You can log in with a recovery code. @@ -48,6 +40,7 @@

+ @section Scripts { @await Html.PartialAsync("_ValidationScriptsPartial") } diff --git a/BTCPayServer/Views/Account/LoginWithRecoveryCode.cshtml b/BTCPayServer/Views/Account/LoginWithRecoveryCode.cshtml index 00d04a8a2..6298b2c96 100644 --- a/BTCPayServer/Views/Account/LoginWithRecoveryCode.cshtml +++ b/BTCPayServer/Views/Account/LoginWithRecoveryCode.cshtml @@ -3,26 +3,35 @@ ViewData["Title"] = "Recovery code verification"; } -

@ViewData["Title"]

-
-

- You have requested to login with a recovery code. This login will not be remembered until you provide - an authenticator app code at login or disable 2FA and login again. -

-
-
-
-
-
- - - + + +
+
+
+
+

@ViewData["Title"]

+
+

+ You have requested to login with a recovery code. This login will not be remembered until you provide + an authenticator app code at login or disable 2FA and login again. +

- - +
+
+
+
+
+ + + +
+ +
+
-
+ + @section Scripts { @await Html.PartialAsync("_ValidationScriptsPartial") -} \ No newline at end of file +} diff --git a/BTCPayServer/Views/Account/LoginWithU2F.cshtml b/BTCPayServer/Views/Account/LoginWithU2F.cshtml new file mode 100644 index 000000000..2e7476467 --- /dev/null +++ b/BTCPayServer/Views/Account/LoginWithU2F.cshtml @@ -0,0 +1,53 @@ +@model BTCPayServer.Services.U2F.Models.LoginWithU2FViewModel + +
+ + + + + + + +
+ +
+
+
+
+

U2F Authentication

+
+ +

Insert your U2F device or a hardware wallet into your computer's USB port. If it has a button, tap on it.

+
+
+
+ +
+
+
+ + + diff --git a/BTCPayServer/Views/Account/SecondaryLogin.cshtml b/BTCPayServer/Views/Account/SecondaryLogin.cshtml new file mode 100644 index 000000000..4c6c2dd5e --- /dev/null +++ b/BTCPayServer/Views/Account/SecondaryLogin.cshtml @@ -0,0 +1,47 @@ +@model SecondaryLoginViewModel +@{ + ViewData["Title"] = "Two-factor/U2F authentication"; +} + +
+
+ @if (Model.LoginWith2FaViewModel != null && Model.LoginWithU2FViewModel != null) + { +
+
+

@ViewData["Title"]

+
+ +
+
+
+ }else if (Model.LoginWith2FaViewModel == null && Model.LoginWithU2FViewModel == null) + { +
+
+

Both 2FA and U2F Authentication Methods are not available. Please go to the https endpoint

+
+
+
+ } + + +
+ @if (Model.LoginWith2FaViewModel != null) + { +
+ +
+ } + @if (Model.LoginWithU2FViewModel != null) + { +
+ +
+ } +
+
+
+@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/BTCPayServer/Views/Manage/AddU2FDevice.cshtml b/BTCPayServer/Views/Manage/AddU2FDevice.cshtml new file mode 100644 index 000000000..83639021e --- /dev/null +++ b/BTCPayServer/Views/Manage/AddU2FDevice.cshtml @@ -0,0 +1,58 @@ +@model BTCPayServer.Services.U2F.Models.AddU2FDeviceViewModel +@{ + ViewData.SetActivePageAndTitle(ManageNavPages.U2F, "Add U2F device"); +} + + + + +
+

Registering U2F Device

+
+

Insert your U2F device or a hardware wallet into your computer's USB port. If it has a button, tap on it.

+ + +
+ + +
+ +@section Scripts { + + + +} diff --git a/BTCPayServer/Views/Manage/ManageNavPages.cs b/BTCPayServer/Views/Manage/ManageNavPages.cs index b1fd60d2f..4c2b9370f 100644 --- a/BTCPayServer/Views/Manage/ManageNavPages.cs +++ b/BTCPayServer/Views/Manage/ManageNavPages.cs @@ -7,6 +7,6 @@ namespace BTCPayServer.Views.Manage { public enum ManageNavPages { - Index, ChangePassword, ExternalLogins, TwoFactorAuthentication, Tokens + Index, ChangePassword, ExternalLogins, TwoFactorAuthentication, U2F } } diff --git a/BTCPayServer/Views/Manage/U2FAuthentication.cshtml b/BTCPayServer/Views/Manage/U2FAuthentication.cshtml new file mode 100644 index 000000000..17cbba2ee --- /dev/null +++ b/BTCPayServer/Views/Manage/U2FAuthentication.cshtml @@ -0,0 +1,47 @@ +@model BTCPayServer.Services.U2F.Models.U2FAuthenticationViewModel +@{ + ViewData.SetActivePageAndTitle(ManageNavPages.U2F, "Manage your registered U2F devices"); +} + + +

Registered U2F Devices

+
+ + + + + + + + + @foreach (var device in Model.Devices) + { + + + + + } + @if (!Model.Devices.Any()) + { + + + + } + + + + + +
NameActions
@device.Name + Remove + +
+ No registered devices +
+ + +
+ +
+
+
diff --git a/BTCPayServer/Views/Manage/_Nav.cshtml b/BTCPayServer/Views/Manage/_Nav.cshtml index 556d9fe57..99060f5bc 100644 --- a/BTCPayServer/Views/Manage/_Nav.cshtml +++ b/BTCPayServer/Views/Manage/_Nav.cshtml @@ -11,5 +11,6 @@ External logins } Two-factor authentication + U2F Authentication
diff --git a/BTCPayServer/wwwroot/vendor/u2f/u2f-api-1.1.js b/BTCPayServer/wwwroot/vendor/u2f/u2f-api-1.1.js new file mode 100644 index 000000000..76d958a1c --- /dev/null +++ b/BTCPayServer/wwwroot/vendor/u2f/u2f-api-1.1.js @@ -0,0 +1,749 @@ +//Copyright 2014-2015 Google Inc. All rights reserved. + +//Use of this source code is governed by a BSD-style +//license that can be found in the LICENSE file or at +//https://developers.google.com/open-source/licenses/bsd + +/** + * @fileoverview The U2F api. + */ +'use strict'; + + +/** + * Namespace for the U2F api. + * @type {Object} + */ +var u2f = u2f || {}; + +/** + * FIDO U2F Javascript API Version + * @number + */ +var js_api_version; + +/** + * The U2F extension id + * @const {string} + */ +// The Chrome packaged app extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the package Chrome app and does not require installing the U2F Chrome extension. + u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; +// The U2F Chrome extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the U2F Chrome extension to authenticate. +// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; + + +/** + * Message types for messsages to/from the extension + * @const + * @enum {string} + */ +u2f.MessageTypes = { + 'U2F_REGISTER_REQUEST': 'u2f_register_request', + 'U2F_REGISTER_RESPONSE': 'u2f_register_response', + 'U2F_SIGN_REQUEST': 'u2f_sign_request', + 'U2F_SIGN_RESPONSE': 'u2f_sign_response', + 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', + 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' +}; + + +/** + * Response status codes + * @const + * @enum {number} + */ +u2f.ErrorCodes = { + 'OK': 0, + 'OTHER_ERROR': 1, + 'BAD_REQUEST': 2, + 'CONFIGURATION_UNSUPPORTED': 3, + 'DEVICE_INELIGIBLE': 4, + 'TIMEOUT': 5 +}; + + +/** + * A message for registration requests + * @typedef {{ + * type: u2f.MessageTypes, + * appId: ?string, + * timeoutSeconds: ?number, + * requestId: ?number + * }} + */ +u2f.U2fRequest; + + +/** + * A message for registration responses + * @typedef {{ + * type: u2f.MessageTypes, + * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), + * requestId: ?number + * }} + */ +u2f.U2fResponse; + + +/** + * An error object for responses + * @typedef {{ + * errorCode: u2f.ErrorCodes, + * errorMessage: ?string + * }} + */ +u2f.Error; + +/** + * Data object for a single sign request. + * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}} + */ +u2f.Transport; + + +/** + * Data object for a single sign request. + * @typedef {Array} + */ +u2f.Transports; + +/** + * Data object for a single sign request. + * @typedef {{ + * version: string, + * challenge: string, + * keyHandle: string, + * appId: string + * }} + */ +u2f.SignRequest; + + +/** + * Data object for a sign response. + * @typedef {{ + * keyHandle: string, + * signatureData: string, + * clientData: string + * }} + */ +u2f.SignResponse; + + +/** + * Data object for a registration request. + * @typedef {{ + * version: string, + * challenge: string + * }} + */ +u2f.RegisterRequest; + + +/** + * Data object for a registration response. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: Transports, + * appId: string + * }} + */ +u2f.RegisterResponse; + + +/** + * Data object for a registered key. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: ?Transports, + * appId: ?string + * }} + */ +u2f.RegisteredKey; + + +/** + * Data object for a get API register response. + * @typedef {{ + * js_api_version: number + * }} + */ +u2f.GetJsApiVersionResponse; + + +//Low level MessagePort API support + +/** + * Sets up a MessagePort to the U2F extension using the + * available mechanisms. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + */ +u2f.getMessagePort = function(callback) { + if (typeof chrome != 'undefined' && chrome.runtime) { + // The actual message here does not matter, but we need to get a reply + // for the callback to run. Thus, send an empty signature request + // in order to get a failure response. + var msg = { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: [] + }; + chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { + if (!chrome.runtime.lastError) { + // We are on a whitelisted origin and can talk directly + // with the extension. + u2f.getChromeRuntimePort_(callback); + } else { + // chrome.runtime was available, but we couldn't message + // the extension directly, use iframe + u2f.getIframePort_(callback); + } + }); + } else if (u2f.isAndroidChrome_()) { + u2f.getAuthenticatorPort_(callback); + } else if (u2f.isIosChrome_()) { + u2f.getIosPort_(callback); + } else { + // chrome.runtime was not available at all, which is normal + // when this origin doesn't have access to any extensions. + u2f.getIframePort_(callback); + } +}; + +/** + * Detect chrome running on android based on the browser's useragent. + * @private + */ +u2f.isAndroidChrome_ = function() { + var userAgent = navigator.userAgent; + return userAgent.indexOf('Chrome') != -1 && + userAgent.indexOf('Android') != -1; +}; + +/** + * Detect chrome running on iOS based on the browser's platform. + * @private + */ +u2f.isIosChrome_ = function() { + return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; +}; + +/** + * Connects directly to the extension via chrome.runtime.connect. + * @param {function(u2f.WrappedChromeRuntimePort_)} callback + * @private + */ +u2f.getChromeRuntimePort_ = function(callback) { + var port = chrome.runtime.connect(u2f.EXTENSION_ID, + {'includeTlsChannelId': true}); + setTimeout(function() { + callback(new u2f.WrappedChromeRuntimePort_(port)); + }, 0); +}; + +/** + * Return a 'port' abstraction to the Authenticator app. + * @param {function(u2f.WrappedAuthenticatorPort_)} callback + * @private + */ +u2f.getAuthenticatorPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedAuthenticatorPort_()); + }, 0); +}; + +/** + * Return a 'port' abstraction to the iOS client app. + * @param {function(u2f.WrappedIosPort_)} callback + * @private + */ +u2f.getIosPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedIosPort_()); + }, 0); +}; + +/** + * A wrapper for chrome.runtime.Port that is compatible with MessagePort. + * @param {Port} port + * @constructor + * @private + */ +u2f.WrappedChromeRuntimePort_ = function(port) { + this.port_ = port; +}; + +/** + * Format and return a sign request compliant with the JS API version supported by the extension. + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatSignRequest_ = + function(appId, challenge, registeredKeys, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: challenge, + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: signRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + appId: appId, + challenge: challenge, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; +}; + +/** + * Format and return a register request compliant with the JS API version supported by the extension.. + * @param {Array} signRequests + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatRegisterRequest_ = + function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + for (var i = 0; i < registerRequests.length; i++) { + registerRequests[i].appId = appId; + } + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: registerRequests[0], + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + signRequests: signRequests, + registerRequests: registerRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + appId: appId, + registerRequests: registerRequests, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; +}; + + +/** + * Posts a message on the underlying channel. + * @param {Object} message + */ +u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { + this.port_.postMessage(message); +}; + + +/** + * Emulates the HTML 5 addEventListener interface. Works only for the + * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedChromeRuntimePort_.prototype.addEventListener = + function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message' || name == 'onmessage') { + this.port_.onMessage.addListener(function(message) { + // Emulate a minimal MessageEvent object + handler({'data': message}); + }); + } else { + console.error('WrappedChromeRuntimePort only supports onMessage'); + } +}; + +/** + * Wrap the Authenticator app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedAuthenticatorPort_ = function() { + this.requestId_ = -1; + this.requestObject_ = null; +} + +/** + * Launch the Authenticator intent. + * @param {Object} message + */ +u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { + var intentUrl = + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + + ';S.request=' + encodeURIComponent(JSON.stringify(message)) + + ';end'; + document.location = intentUrl; +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { + return "WrappedAuthenticatorPort_"; +}; + + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message') { + var self = this; + /* Register a callback to that executes when + * chrome injects the response. */ + window.addEventListener( + 'message', self.onRequestUpdate_.bind(self, handler), false); + } else { + console.error('WrappedAuthenticatorPort only supports message'); + } +}; + +/** + * Callback invoked when a response is received from the Authenticator. + * @param function({data: Object}) callback + * @param {Object} message message Object + */ +u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = + function(callback, message) { + var messageObject = JSON.parse(message.data); + var intentUrl = messageObject['intentURL']; + + var errorCode = messageObject['errorCode']; + var responseObject = null; + if (messageObject.hasOwnProperty('data')) { + responseObject = /** @type {Object} */ ( + JSON.parse(messageObject['data'])); + } + + callback({'data': responseObject}); +}; + +/** + * Base URL for intents to Authenticator. + * @const + * @private + */ +u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = + 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; + +/** + * Wrap the iOS client app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedIosPort_ = function() {}; + +/** + * Launch the iOS client app request + * @param {Object} message + */ +u2f.WrappedIosPort_.prototype.postMessage = function(message) { + var str = JSON.stringify(message); + var url = "u2f://auth?" + encodeURI(str); + location.replace(url); +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedIosPort_.prototype.getPortType = function() { + return "WrappedIosPort_"; +}; + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name !== 'message') { + console.error('WrappedIosPort only supports message'); + } +}; + +/** + * Sets up an embedded trampoline iframe, sourced from the extension. + * @param {function(MessagePort)} callback + * @private + */ +u2f.getIframePort_ = function(callback) { + // Create the iframe + var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; + var iframe = document.createElement('iframe'); + iframe.src = iframeOrigin + '/u2f-comms.html'; + iframe.origin = iframeOrigin; + iframe.setAttribute('style', 'display:none'); + document.body.appendChild(iframe); + + var channel = new MessageChannel(); + var ready = function(message) { + if (message.data == 'ready') { + channel.port1.removeEventListener('message', ready); + callback(channel.port1); + } else { + console.error('First event on iframe port was not "ready"'); + } + }; + channel.port1.addEventListener('message', ready); + channel.port1.start(); + + iframe.addEventListener('load', function() { + // Deliver the port to the iframe and initialize + iframe.contentWindow.postMessage('init', null, [channel.port2]); + }); +}; + + +//High-level JS API + +/** + * Default extension response timeout in seconds. + * @const + */ +u2f.EXTENSION_TIMEOUT_SEC = 30; + +/** + * A singleton instance for a MessagePort to the extension. + * @type {MessagePort|u2f.WrappedChromeRuntimePort_} + * @private + */ +u2f.port_ = null; + +/** + * Callbacks waiting for a port + * @type {Array} + * @private + */ +u2f.waitingForPort_ = []; + +/** + * A counter for requestIds. + * @type {number} + * @private + */ +u2f.reqCounter_ = 0; + +/** + * A map from requestIds to client callbacks + * @type {Object.} + * @private + */ +u2f.callbackMap_ = {}; + +/** + * Creates or retrieves the MessagePort singleton to use. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + * @private + */ +u2f.getPortSingleton_ = function(callback) { + if (u2f.port_) { + callback(u2f.port_); + } else { + if (u2f.waitingForPort_.length == 0) { + u2f.getMessagePort(function(port) { + u2f.port_ = port; + u2f.port_.addEventListener('message', + /** @type {function(Event)} */ (u2f.responseHandler_)); + + // Careful, here be async callbacks. Maybe. + while (u2f.waitingForPort_.length) + u2f.waitingForPort_.shift()(u2f.port_); + }); + } + u2f.waitingForPort_.push(callback); + } +}; + +/** + * Handles response messages from the extension. + * @param {MessageEvent.} message + * @private + */ +u2f.responseHandler_ = function(message) { + var response = message.data; + var reqId = response['requestId']; + if (!reqId || !u2f.callbackMap_[reqId]) { + console.error('Unknown or missing requestId in response.'); + return; + } + var cb = u2f.callbackMap_[reqId]; + delete u2f.callbackMap_[reqId]; + cb(response['responseData']); +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the sign request. + * @param {string=} appId + * @param {string=} challenge + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual sign request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual sign request in the supported API version. + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * @param {string=} appId + * @param {string=} challenge + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the register request. + * @param {string=} appId + * @param {Array} registerRequests + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual register request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual register request in the supported API version. + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * @param {string=} appId + * @param {Array} registerRequests + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatRegisterRequest_( + appId, registeredKeys, registerRequests, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + + +/** + * Dispatches a message to the extension to find out the supported + * JS API version. + * If the user is on a mobile phone and is thus using Google Authenticator instead + * of the Chrome extension, don't send the request and simply return 0. + * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.getApiVersion = function(callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + // If we are using Android Google Authenticator or iOS client app, + // do not fire an intent to ask which JS API version to use. + if (port.getPortType) { + var apiVersion; + switch (port.getPortType()) { + case 'WrappedIosPort_': + case 'WrappedAuthenticatorPort_': + apiVersion = 1.1; + break; + + default: + apiVersion = 0; + break; + } + callback({ 'js_api_version': apiVersion }); + return; + } + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var req = { + type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, + timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), + requestId: reqId + }; + port.postMessage(req); + }); +}; From 3cd37682d396e61d75dfd7c7a6d2ced9c06d0954 Mon Sep 17 00:00:00 2001 From: Amit Singh Date: Thu, 2 May 2019 17:32:01 +0530 Subject: [PATCH 006/243] [BUG FIX]: Coinswitch exchange with altcoins popup not showing bug fix (#804) --- BTCPayServer/wwwroot/checkout/coinswitch.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/BTCPayServer/wwwroot/checkout/coinswitch.html b/BTCPayServer/wwwroot/checkout/coinswitch.html index 899405a48..448e4007c 100644 --- a/BTCPayServer/wwwroot/checkout/coinswitch.html +++ b/BTCPayServer/wwwroot/checkout/coinswitch.html @@ -39,7 +39,10 @@ to_amount: parseFloat(toCurrencyDue), state: orderId }; - waitForCoinSwitch(); + + payment.on('Exchange:Ready', function(){ + waitForCoinSwitch(); + }) function waitForCoinSwitch() { if (typeof payment.open !== "function") { From 6918b8a291d24c1abebcd3f825c68f3df738057c Mon Sep 17 00:00:00 2001 From: rockstardev Date: Mon, 29 Apr 2019 22:45:33 -0500 Subject: [PATCH 007/243] Extracting payment details population, refactoring invoice data load --- .../Controllers/InvoiceController.UI.cs | 45 ++- .../Models/InvoicingModels/InvoicesModel.cs | 2 + BTCPayServer/Views/Invoice/Invoice.cshtml | 354 +++++++----------- .../Invoice/InvoicePaymentsPartial.cshtml | 97 +++++ .../Views/Invoice/ListInvoices.cshtml | 22 +- 5 files changed, 279 insertions(+), 241 deletions(-) create mode 100644 BTCPayServer/Views/Invoice/InvoicePaymentsPartial.cshtml diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index fb8b99ff2..90a6489a5 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -47,9 +47,9 @@ namespace BTCPayServer.Controllers if (invoice == null) return NotFound(); - var dto = invoice.EntityToDTO(_NetworkProvider); + var prodInfo = invoice.ProductInformation; var store = await _StoreRepository.FindStore(invoice.StoreId); - InvoiceDetailsModel model = new InvoiceDetailsModel() + var model = new InvoiceDetailsModel() { StoreName = store.StoreName, StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }), @@ -65,20 +65,39 @@ namespace BTCPayServer.Controllers MonitoringDate = invoice.MonitoringExpiration, OrderId = invoice.OrderId, BuyerInformation = invoice.BuyerInformation, - Fiat = _CurrencyNameTable.DisplayFormatCurrency(dto.Price, dto.Currency), - TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.TaxIncluded, dto.Currency), + Fiat = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.Price, prodInfo.Currency), + TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.TaxIncluded, prodInfo.Currency), NotificationEmail = invoice.NotificationEmail, NotificationUrl = invoice.NotificationURL, RedirectUrl = invoice.RedirectURL, ProductInformation = invoice.ProductInformation, StatusException = invoice.ExceptionStatus, Events = invoice.Events, - PosData = PosDataParser.ParsePosData(dto.PosData) + PosData = PosDataParser.ParsePosData(invoice.PosData), + StatusMessage = StatusMessage }; + model.Addresses = invoice.HistoricalAddresses.Select(h => new InvoiceDetailsModel.AddressModel + { + Destination = h.GetAddress(), + PaymentMethod = ToString(h.GetPaymentMethodId()), + Current = !h.UnAssigned.HasValue + }).ToArray(); + + var details = await InvoicePopulatePayments(invoice); + model.CryptoPayments = details.CryptoPayments; + model.OnChainPayments = details.OnChainPayments; + model.OffChainPayments = details.OffChainPayments; + + return View(model); + } + + private async Task InvoicePopulatePayments(InvoiceEntity invoice) + { + var model = new InvoiceDetailsModel(); + foreach (var data in invoice.GetPaymentMethods(null)) { - var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == data.GetId()); var accounting = data.Calculate(); var paymentMethodId = data.GetId(); var cryptoPayment = new InvoiceDetailsModel.CryptoPayment(); @@ -93,7 +112,6 @@ namespace BTCPayServer.Controllers cryptoPayment.Address = onchainMethod.DepositAddress; } cryptoPayment.Rate = ExchangeRate(data); - cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21; model.CryptoPayments.Add(cryptoPayment); } @@ -149,16 +167,10 @@ namespace BTCPayServer.Controllers }) .ToArray(); await Task.WhenAll(onChainPayments); - model.Addresses = invoice.HistoricalAddresses.Select(h => new InvoiceDetailsModel.AddressModel - { - Destination = h.GetAddress(), - PaymentMethod = ToString(h.GetPaymentMethodId()), - Current = !h.UnAssigned.HasValue - }).ToArray(); model.OnChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType().ToList(); model.OffChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType().ToList(); - model.StatusMessage = StatusMessage; - return View(model); + + return model; } private string ToString(PaymentMethodId paymentMethodId) @@ -494,7 +506,8 @@ namespace BTCPayServer.Controllers RedirectUrl = invoice.RedirectURL ?? string.Empty, AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.Price, invoice.ProductInformation.Currency), CanMarkInvalid = state.CanMarkInvalid(), - CanMarkComplete = state.CanMarkComplete() + CanMarkComplete = state.CanMarkComplete(), + Details = await InvoicePopulatePayments(invoice) }); } model.Total = await counting; diff --git a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs index b0faaba49..42dc8f6f1 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs @@ -33,5 +33,7 @@ namespace BTCPayServer.Models.InvoicingModels public string ExceptionStatus { get; set; } public string AmountCurrency { get; set; } public string StatusMessage { get; set; } + + public InvoiceDetailsModel Details { get; set; } } } diff --git a/BTCPayServer/Views/Invoice/Invoice.cshtml b/BTCPayServer/Views/Invoice/Invoice.cshtml index 00e7e13ac..d663678ac 100644 --- a/BTCPayServer/Views/Invoice/Invoice.cshtml +++ b/BTCPayServer/Views/Invoice/Invoice.cshtml @@ -42,257 +42,163 @@
-
-
-

Information

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Store@Model.StoreName
Id@Model.Id
State@Model.State
Created date@Model.CreatedDate.ToBrowserDate()
Expiration date@Model.ExpirationDate.ToBrowserDate()
Monitoring date@Model.MonitoringDate.ToBrowserDate()
Transaction speed@Model.TransactionSpeed
Refund email@Model.RefundEmail
Order Id@Model.OrderId
Total fiat due@Model.Fiat
Notification Email@Model.NotificationEmail
Notification Url@Model.NotificationUrl
Redirect Url@Model.RedirectUrl
-
- -
-

Buyer information

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Name@Model.BuyerInformation.BuyerName
Email@Model.BuyerInformation.BuyerEmail
Phone@Model.BuyerInformation.BuyerPhone
Address 1@Model.BuyerInformation.BuyerAddress1
Address 2@Model.BuyerInformation.BuyerAddress2
City@Model.BuyerInformation.BuyerCity
State@Model.BuyerInformation.BuyerState
Country@Model.BuyerInformation.BuyerCountry
Zip@Model.BuyerInformation.BuyerZip
- @if (Model.PosData.Count == 0) - { -

Product information

- - - - - - - - - - - - - - - - - -
Item code@Model.ProductInformation.ItemCode
Item Description@Model.ProductInformation.ItemDesc
Price@Model.Fiat
Tax included@Model.TaxIncluded
- } -
-
- - @if (Model.PosData.Count != 0) - {
-

Product information

+

Information

- - + + - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + +
Item code@Model.ProductInformation.ItemCodeStore@Model.StoreName
Item Description@Model.ProductInformation.ItemDescId@Model.Id
PriceState@Model.State
Created date@Model.CreatedDate.ToBrowserDate()
Expiration date@Model.ExpirationDate.ToBrowserDate()
Monitoring date@Model.MonitoringDate.ToBrowserDate()
Transaction speed@Model.TransactionSpeed
Refund email@Model.RefundEmail
Order Id@Model.OrderId
Total fiat due @Model.Fiat
Tax included@Model.TaxIncludedNotification Email@Model.NotificationEmail
Notification Url@Model.NotificationUrl
Redirect Url@Model.RedirectUrl
-
-

Point of Sale Data

- -
-
- } -
-
-

Paid summary

+
+

Buyer information

- - - - - - - - @if (Model.StatusException == InvoiceExceptionStatus.PaidOver) - { - - } - - - - @foreach (var payment in Model.CryptoPayments) - { - - - - - - - @if (Model.StatusException == InvoiceExceptionStatus.PaidOver) - { - - } - - } - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Payment methodAddressRatePaidDueOverpaid
@payment.PaymentMethod@payment.Address@payment.Rate@payment.Paid@payment.Due@payment.Overpaid
Name@Model.BuyerInformation.BuyerName
Email@Model.BuyerInformation.BuyerEmail
Phone@Model.BuyerInformation.BuyerPhone
Address 1@Model.BuyerInformation.BuyerAddress1
Address 2@Model.BuyerInformation.BuyerAddress2
City@Model.BuyerInformation.BuyerCity
State@Model.BuyerInformation.BuyerState
Country@Model.BuyerInformation.BuyerCountry
Zip@Model.BuyerInformation.BuyerZip
+ @if (Model.PosData.Count == 0) + { +

Product information

+ + + + + + + + + + + + + + + + + +
Item code@Model.ProductInformation.ItemCode
Item Description@Model.ProductInformation.ItemDesc
Price@Model.Fiat
Tax included@Model.TaxIncluded
+ }
- @if (Model.OnChainPayments.Count > 0) + + @if (Model.PosData.Count != 0) {
-
-

On-Chain payments

- - - - - - - - - - - @foreach (var payment in Model.OnChainPayments) - { - var replaced = payment.Replaced ? "class='linethrough'" : ""; - - - - - - - } - -
CryptoDeposit addressTransaction IdConfirmations
@payment.Crypto@payment.DepositAddress - - @payment.TransactionId - - @payment.Confirmations
-
-
- } - @if (Model.OffChainPayments.Count > 0) - { -
-
-

Off-Chain payments

+
+

Product information

- - - - - - - - @foreach (var payment in Model.OffChainPayments) - { - - - - - } - + + + + + + + + + + + + + + + +
CryptoBOLT11
@payment.Crypto@payment.BOLT11
Item code@Model.ProductInformation.ItemCode
Item Description@Model.ProductInformation.ItemDesc
Price@Model.Fiat
Tax included@Model.TaxIncluded
+
+

Point of Sale Data

+ +
} + +

Events

diff --git a/BTCPayServer/Views/Invoice/InvoicePaymentsPartial.cshtml b/BTCPayServer/Views/Invoice/InvoicePaymentsPartial.cshtml new file mode 100644 index 000000000..6ba393f12 --- /dev/null +++ b/BTCPayServer/Views/Invoice/InvoicePaymentsPartial.cshtml @@ -0,0 +1,97 @@ +@model InvoiceDetailsModel + +
+
+

Paid summary

+ + + + + + + + + @if (Model.StatusException == InvoiceExceptionStatus.PaidOver) + { + + } + + + + @foreach (var payment in Model.CryptoPayments) + { + + + + + + + @if (Model.StatusException == InvoiceExceptionStatus.PaidOver) + { + + } + + } + +
Payment methodAddressRatePaidDueOverpaid
@payment.PaymentMethod@payment.Address@payment.Rate@payment.Paid@payment.Due@payment.Overpaid
+
+
+@if (Model.OnChainPayments.Count > 0) +{ +
+
+

On-Chain payments

+ + + + + + + + + + + @foreach (var payment in Model.OnChainPayments) + { + var replaced = payment.Replaced ? "class='linethrough'" : ""; + + + + + + + } + +
CryptoDeposit addressTransaction IdConfirmations
@payment.Crypto@payment.DepositAddress + + @payment.TransactionId + + @payment.Confirmations
+
+
+} +@if (Model.OffChainPayments.Count > 0) +{ +
+
+

Off-Chain payments

+ + + + + + + + + @foreach (var payment in Model.OffChainPayments) + { + + + + + } + +
CryptoBOLT11
@payment.Crypto@payment.BOLT11
+
+
+} diff --git a/BTCPayServer/Views/Invoice/ListInvoices.cshtml b/BTCPayServer/Views/Invoice/ListInvoices.cshtml index 011be41e2..fb75c2063 100644 --- a/BTCPayServer/Views/Invoice/ListInvoices.cshtml +++ b/BTCPayServer/Views/Invoice/ListInvoices.cshtml @@ -103,7 +103,7 @@ @invoice.Date.ToBrowserDate() - + @if (invoice.RedirectUrl != string.Empty) @@ -151,6 +151,15 @@
} Details + + Expand + + + + +
+ +
} @@ -219,5 +228,16 @@ this.href = this.href.replace("timezoneoffset=0", "timezoneoffset=" + timezoneOffset); }); }) + + function detailsToggle(invoiceId) { + $("#invoice_" + invoiceId).toggle(); + } + + From 1d3ff143d2d17f8e910292311ccc9bc09cf938e5 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Mon, 29 Apr 2019 23:25:11 -0500 Subject: [PATCH 008/243] Tweaking UI, expanding details and max width on order id --- .../Views/Invoice/ListInvoices.cshtml | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/BTCPayServer/Views/Invoice/ListInvoices.cshtml b/BTCPayServer/Views/Invoice/ListInvoices.cshtml index fb75c2063..16398243b 100644 --- a/BTCPayServer/Views/Invoice/ListInvoices.cshtml +++ b/BTCPayServer/Views/Invoice/ListInvoices.cshtml @@ -89,7 +89,7 @@ - OrderId + OrderId InvoiceId Status Amount @@ -105,10 +105,10 @@ @invoice.Date.ToBrowserDate() - + @if (invoice.RedirectUrl != string.Empty) { - @invoice.OrderId + @invoice.OrderId } else { @@ -150,9 +150,13 @@ }
} +   Details - - Expand + @**@ +   + + + @@ -229,8 +233,17 @@ }); }) - function detailsToggle(invoiceId) { - $("#invoice_" + invoiceId).toggle(); + function detailsToggle(sender, invoiceId) { + $("#invoice_" + invoiceId).toggle(0, function () { + var detailsRow = $(this); + var btnToggle = $(sender).children().first(); + if (detailsRow.is(':visible')) { + btnToggle.removeClass('fa-angle-double-down').addClass('fa-angle-double-up'); + } else { + btnToggle.removeClass('fa-angle-double-up').addClass('fa-angle-double-down'); + } + }); + return false; } @@ -239,5 +252,13 @@ font-size: 15px; font-weight: bold; } + + .wraptext200 { + max-width: 200px; + text-overflow: ellipsis; + overflow: hidden; + display: block; + white-space: nowrap; + } From 4bc03fbf06226488ad82a07b7b309b64be587e54 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Wed, 1 May 2019 15:33:46 -0500 Subject: [PATCH 009/243] Code coloring invoice states --- .../Controllers/InvoiceController.UI.cs | 3 +- .../Models/InvoicingModels/InvoicesModel.cs | 4 +- .../Views/Invoice/ListInvoices.cshtml | 81 +++++++++++++++---- 3 files changed, 69 insertions(+), 19 deletions(-) diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 90a6489a5..6c6d80e00 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -498,7 +498,8 @@ namespace BTCPayServer.Controllers var state = invoice.GetInvoiceState(); model.Invoices.Add(new InvoiceModel() { - Status = state.ToString(), + Status = invoice.Status, + StatusString = state.ToString(), ShowCheckout = invoice.Status == InvoiceStatus.New, Date = invoice.InvoiceTime, InvoiceId = invoice.Id, diff --git a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs index 42dc8f6f1..8aa5b0a28 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoicesModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Services.Invoices; namespace BTCPayServer.Models.InvoicingModels { @@ -25,7 +26,8 @@ namespace BTCPayServer.Models.InvoicingModels public string RedirectUrl { get; set; } public string InvoiceId { get; set; } - public string Status { get; set; } + public InvoiceStatus Status { get; set; } + public string StatusString { get; set; } public bool CanMarkComplete { get; set; } public bool CanMarkInvalid { get; set; } public bool CanMarkStatus => CanMarkComplete || CanMarkInvalid; diff --git a/BTCPayServer/Views/Invoice/ListInvoices.cshtml b/BTCPayServer/Views/Invoice/ListInvoices.cshtml index 16398243b..27ba44da9 100644 --- a/BTCPayServer/Views/Invoice/ListInvoices.cshtml +++ b/BTCPayServer/Views/Invoice/ListInvoices.cshtml @@ -116,25 +116,13 @@ } @invoice.InvoiceId - @invoice.Status - @invoice.AmountCurrency - - @if (invoice.ShowCheckout) - { - - Checkout - [^] - @if (!invoice.CanMarkStatus) - { - - - } - - } + @if (invoice.CanMarkStatus) { - + } + else + { + @invoice.StatusString + } + + @invoice.AmountCurrency + + @if (invoice.ShowCheckout) + { + + Checkout + [^] + @if (!invoice.CanMarkStatus) + { + - + } + + }   Details @**@ @@ -260,5 +266,46 @@ display: block; white-space: nowrap; } + + .pavpill { + display: inline-block; + padding: 0.3em 0.5em; + font-size: 85%; + font-weight: 500; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25rem; + } + + .pavpill.dropdown-toggle { + cursor: pointer; + } + + .pavpil-new { + background: #d4edda; + color: #000; + } + + .pavpil-expired { + background: #eee; + color: #000; + } + + .pavpil-invalid { + background: #c94a47; + color: #fff; + } + + .pavpil-confirmed, .pavpil-paid { + background: #f1c332; + color: #000; + } + + .pavpil-complete { + background: #329f80; + color: #fff; + } From b5f4739ae5a3007282e9b6682b109b2dcef64ef3 Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Thu, 2 May 2019 14:29:51 +0200 Subject: [PATCH 010/243] Allow invoice creation to only allow specific payment methods in UI (#792) * allow invoice creation to only allow specific payment methods * add test * reuse existing feature * final fixes --- BTCPayServer.Tests/UnitTest1.cs | 34 +++++++++++++++++++ .../Controllers/InvoiceController.UI.cs | 26 +++++++++++++- BTCPayServer/Controllers/InvoiceController.cs | 1 - .../InvoicingModels/CreateInvoiceModel.cs | 11 ++++++ .../Views/Invoice/CreateInvoice.cshtml | 5 +++ 5 files changed, 75 insertions(+), 2 deletions(-) diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index b11a366e7..a0f36f3f4 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -2609,6 +2609,40 @@ donation: Assert.Equal(StatusMessageModel.StatusSeverity.Success, parsed.Severity); } + + [Fact] + [Trait("Integration", "Integration")] + public async Task CanCreateInvoiceWithSpecificPaymentMethods() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + await tester.EnsureChannelsSetup(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterLightningNode("BTC", LightningConnectionType.Charge); + user.RegisterDerivationScheme("BTC"); + user.RegisterDerivationScheme("LTC"); + + var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC")); + Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count); + + + invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC") + { + SupportedTransactionCurrencies = new Dictionary() + { + {"BTC", new InvoiceSupportedTransactionCurrency() + { + Enabled = true + }} + } + }); + + Assert.Equal(1, invoice.SupportedTransactionCurrencies.Count); + } + } + [Fact] diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 6c6d80e00..320257ac9 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -571,7 +571,16 @@ namespace BTCPayServer.Controllers StatusMessage = "Error: You need to create at least one store before creating a transaction"; return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores"); } - return View(new CreateInvoiceModel() { Stores = stores }); + + var paymentMethods = new SelectList(_NetworkProvider.GetAll().SelectMany(network => new[] + { + new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike), + new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike) + }).Select(id => new SelectListItem(id.ToString(true), id.ToString(false))), + nameof(SelectListItem.Value), + nameof(SelectListItem.Text)); + + return View(new CreateInvoiceModel() { Stores = stores, AvailablePaymentMethods = paymentMethods}); } [HttpPost] @@ -582,6 +591,16 @@ namespace BTCPayServer.Controllers { var stores = await _StoreRepository.GetStoresByUserId(GetUserId()); model.Stores = new SelectList(stores, nameof(StoreData.Id), nameof(StoreData.StoreName), model.StoreId); + + var paymentMethods = new SelectList(_NetworkProvider.GetAll().SelectMany(network => new[] + { + new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike), + new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike) + }).Select(id => new SelectListItem(id.ToString(true), id.ToString(false))), + nameof(SelectListItem.Value), + nameof(SelectListItem.Text)); + model.AvailablePaymentMethods = paymentMethods; + var store = stores.FirstOrDefault(s => s.Id == model.StoreId); if (store == null) { @@ -603,6 +622,7 @@ namespace BTCPayServer.Controllers ModelState.AddModelError(nameof(model.StoreId), "You need to configure the derivation scheme in order to create an invoice"); return View(model); } + if (StatusMessage != null) { @@ -626,6 +646,10 @@ namespace BTCPayServer.Controllers ItemDesc = model.ItemDesc, FullNotifications = true, BuyerEmail = model.BuyerEmail, + SupportedTransactionCurrencies = model.SupportedTransactionCurrencies?.ToDictionary(s => s, s => new InvoiceSupportedTransactionCurrency() + { + Enabled =true + }) }, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken); StatusMessage = $"Invoice {result.Data.Id} just created!"; diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index 64be2a1c5..4a64fe4d0 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -160,7 +160,6 @@ namespace BTCPayServer.Controllers var rateRules = storeBlob.GetRateRules(_NetworkProvider); var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules, cancellationToken); - var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair); var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider) .Where(s => !excludeFilter.Match(s.PaymentId)) diff --git a/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs b/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs index 8df1a47f9..9fd8b923e 100644 --- a/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs +++ b/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs @@ -70,5 +70,16 @@ namespace BTCPayServer.Models.InvoicingModels get; set; } + + public List SupportedTransactionCurrencies + { + get; + set; + } + public SelectList AvailablePaymentMethods + { + get; + set; + } } } diff --git a/BTCPayServer/Views/Invoice/CreateInvoice.cshtml b/BTCPayServer/Views/Invoice/CreateInvoice.cshtml index 5a8f5d158..e1f2a003b 100644 --- a/BTCPayServer/Views/Invoice/CreateInvoice.cshtml +++ b/BTCPayServer/Views/Invoice/CreateInvoice.cshtml @@ -67,6 +67,11 @@
+
+ + + +
From a20db7f34161dd5cf0a424d2ce6493205c8e276a Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Thu, 2 May 2019 21:35:28 +0900 Subject: [PATCH 011/243] bump nbx --- BTCPayServer.Tests/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index 4507adc40..8e19a544a 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -71,7 +71,7 @@ services: nbxplorer: - image: nicolasdorier/nbxplorer:2.0.0.35 + image: nicolasdorier/nbxplorer:2.0.0.36 restart: unless-stopped ports: - "32838:32838" From 7fadb4c5ad72ae921749a5d2e12620ad675c295e Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Thu, 2 May 2019 21:38:39 +0900 Subject: [PATCH 012/243] Remove some annoying warnings --- BTCPayServer.Tests/UnitTest1.cs | 2 +- BTCPayServer/Hosting/ResourceBundleProvider.cs | 4 ++-- BTCPayServer/Services/HardwareWalletService.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index a0f36f3f4..6951ce720 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -2639,7 +2639,7 @@ donation: } }); - Assert.Equal(1, invoice.SupportedTransactionCurrencies.Count); + Assert.Single(invoice.SupportedTransactionCurrencies); } } diff --git a/BTCPayServer/Hosting/ResourceBundleProvider.cs b/BTCPayServer/Hosting/ResourceBundleProvider.cs index 0d7875f1a..6f10cd214 100644 --- a/BTCPayServer/Hosting/ResourceBundleProvider.cs +++ b/BTCPayServer/Hosting/ResourceBundleProvider.cs @@ -28,8 +28,8 @@ namespace BTCPayServer.Hosting return JArray.Parse(content).OfType() .Select(jobj => new Bundle() { - Name = jobj.Property("name")?.Value.Value() ?? jobj.Property("outputFileName").Value.Value(), - OutputFileUrl = Path.Combine(hosting.ContentRootPath, jobj.Property("outputFileName").Value.Value()) + Name = jobj.Property("name", StringComparison.OrdinalIgnoreCase)?.Value.Value() ?? jobj.Property("outputFileName", StringComparison.OrdinalIgnoreCase).Value.Value(), + OutputFileUrl = Path.Combine(hosting.ContentRootPath, jobj.Property("outputFileName", StringComparison.OrdinalIgnoreCase).Value.Value()) }).ToDictionary(o => o.Name, o => o); } }, true); diff --git a/BTCPayServer/Services/HardwareWalletService.cs b/BTCPayServer/Services/HardwareWalletService.cs index dbdb1ec07..08d19dc26 100644 --- a/BTCPayServer/Services/HardwareWalletService.cs +++ b/BTCPayServer/Services/HardwareWalletService.cs @@ -118,7 +118,7 @@ namespace BTCPayServer.Services if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet) throw new Exception($"The opened ledger app does not seems to support {network.NBitcoinNetwork.Name}."); } - var fingerprint = onlyChaincode ? new byte[4] : (await ledger.GetWalletPubKeyAsync(account.Parent, cancellation: cancellation)).UncompressedPublicKey.Compress().Hash.ToBytes().Take(4).ToArray(); + var fingerprint = onlyChaincode ? default : (await ledger.GetWalletPubKeyAsync(account.Parent, cancellation: cancellation)).UncompressedPublicKey.Compress().GetHDFingerPrint(); var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(), pubKey.ChainCode, (byte)account.Indexes.Length, fingerprint, account.Indexes.Last()).GetWif(network.NBitcoinNetwork); return extpubkey; } From e169b851ee45d2f30daeb63c34fa7e693da62107 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Thu, 2 May 2019 21:44:16 +0900 Subject: [PATCH 013/243] Remove another warning --- BTCPayServer/Security/BitpayAuthentication.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/Security/BitpayAuthentication.cs b/BTCPayServer/Security/BitpayAuthentication.cs index d33ff5910..01c85dd46 100644 --- a/BTCPayServer/Security/BitpayAuthentication.cs +++ b/BTCPayServer/Security/BitpayAuthentication.cs @@ -141,7 +141,7 @@ namespace BTCPayServer.Security { try { - token = JObject.Parse(body)?.Property("token")?.Value?.Value(); + token = JObject.Parse(body)?.Property("token", StringComparison.OrdinalIgnoreCase)?.Value?.Value(); } catch { } } From 957fbdb907d56240ad7c78c0666fb8d01495c14f Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 3 May 2019 10:18:08 +0900 Subject: [PATCH 014/243] Update NBitcoin, NBXplorer, Bitcoin Core --- BTCPayServer.Tests/docker-compose.yml | 6 +++--- BTCPayServer/BTCPayServer.csproj | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index 8e19a544a..da5b2a475 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -38,7 +38,7 @@ services: # The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services dev: - image: btcpayserver/bitcoin:0.17.0 + image: btcpayserver/bitcoin:0.18.0 environment: BITCOIN_NETWORK: regtest BITCOIN_EXTRA_ARGS: | @@ -55,7 +55,7 @@ services: - merchant_lnd devlnd: - image: btcpayserver/bitcoin:0.17.0 + image: btcpayserver/bitcoin:0.18.0 environment: BITCOIN_NETWORK: regtest BITCOIN_EXTRA_ARGS: | @@ -71,7 +71,7 @@ services: nbxplorer: - image: nicolasdorier/nbxplorer:2.0.0.36 + image: nicolasdorier/nbxplorer:2.0.0.37 restart: unless-stopped ports: - "32838:32838" diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 7889bdcb4..1b6e29003 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -47,10 +47,10 @@ all runtime; build; native; contentfiles; analyzers - + - + From 778dcf97b1fe4294d91e8f6832419ed1cece5b6c Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 3 May 2019 11:04:19 +0900 Subject: [PATCH 015/243] update docker compose for bitcoin --- BTCPayServer.Tests/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index da5b2a475..08f740b4f 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -98,13 +98,13 @@ services: bitcoind: restart: unless-stopped - image: btcpayserver/bitcoin:0.17.0 + image: btcpayserver/bitcoin:0.18.0 environment: BITCOIN_NETWORK: regtest BITCOIN_EXTRA_ARGS: |- rpcuser=ceiwHEbqWI83 rpcpassword=DwubwWsoo3 - rpcport=43782 + rpcbind=0.0.0.0:43782 port=39388 whitelist=0.0.0.0/0 zmqpubrawblock=tcp://0.0.0.0:28332 From 1f04e4e6be4481349cf0dc90734226c74f67db15 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 3 May 2019 11:10:01 +0900 Subject: [PATCH 016/243] Add rpcport for bitcoin-cli --- BTCPayServer.Tests/docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index 08f740b4f..86b5000a8 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -104,6 +104,7 @@ services: BITCOIN_EXTRA_ARGS: |- rpcuser=ceiwHEbqWI83 rpcpassword=DwubwWsoo3 + rpcport=43782 rpcbind=0.0.0.0:43782 port=39388 whitelist=0.0.0.0/0 From d32a24004e64aee733edcea6151703c87aac6007 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 3 May 2019 12:59:11 +0900 Subject: [PATCH 017/243] Fix test --- BTCPayServer.Tests/ServerTester.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/BTCPayServer.Tests/ServerTester.cs b/BTCPayServer.Tests/ServerTester.cs index 0b75db473..24c22887f 100644 --- a/BTCPayServer.Tests/ServerTester.cs +++ b/BTCPayServer.Tests/ServerTester.cs @@ -45,6 +45,7 @@ namespace BTCPayServer.Tests NetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest); ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork); + ExplorerNode.ScanRPCCapabilities(); LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork); ExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("BTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_BTCNBXPLORERURL", "http://127.0.0.1:32838/"))); From e2b2cf0175be7341c7ead58f5eafbb168287cdcc Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Sat, 4 May 2019 15:57:44 +0000 Subject: [PATCH 018/243] Do not drop column in u2f migration if not possible (#813) closes #812 --- .../20190425081749_AddU2fDevices.cs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/BTCPayServer/Migrations/20190425081749_AddU2fDevices.cs b/BTCPayServer/Migrations/20190425081749_AddU2fDevices.cs index 4d5575697..7b90d6cca 100644 --- a/BTCPayServer/Migrations/20190425081749_AddU2fDevices.cs +++ b/BTCPayServer/Migrations/20190425081749_AddU2fDevices.cs @@ -7,9 +7,12 @@ namespace BTCPayServer.Migrations { protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.DropColumn( - name: "Facade", - table: "PairedSINData"); + if (this.SupportDropColumn(migrationBuilder.ActiveProvider)) + { + migrationBuilder.DropColumn( + name: "Facade", + table: "PairedSINData"); + } migrationBuilder.CreateTable( name: "U2FDevices", @@ -44,11 +47,14 @@ namespace BTCPayServer.Migrations { migrationBuilder.DropTable( name: "U2FDevices"); - - migrationBuilder.AddColumn( - name: "Facade", - table: "PairedSINData", - nullable: true); + //if it did not support dropping it, then it is still here and re-adding it would throw + if (this.SupportDropColumn(migrationBuilder.ActiveProvider)) + { + migrationBuilder.AddColumn( + name: "Facade", + table: "PairedSINData", + nullable: true); + } } } } From 08bf4faeeea0f582f18862cfe4e854b6608d163b Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 7 May 2019 08:21:34 +0900 Subject: [PATCH 019/243] Pass the hint change address to hardware wallet (useful in care of send-to-self where the underlying wallet support only output belonging to self) --- BTCPayServer/Controllers/WalletsController.cs | 14 +++++++------- BTCPayServer/Services/HardwareWalletService.cs | 6 ++++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index 8b271a69b..7255ea76d 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -504,17 +504,17 @@ namespace BTCPayServer.Controllers { psbtRequest.ExplicitChangeAddress = destinationPSBT.Destination; } - var psbt = (await nbx.CreatePSBTAsync(derivationScheme, psbtRequest, normalOperationTimeout.Token))?.PSBT; + var psbt = (await nbx.CreatePSBTAsync(derivationScheme, psbtRequest, normalOperationTimeout.Token)); if (psbt == null) throw new Exception("You need to update your version of NBXplorer"); if (network.MinFee != null) { - psbt.TryGetFee(out var fee); + psbt.PSBT.TryGetFee(out var fee); if (fee < network.MinFee) { psbtRequest.FeePreference = new FeePreference() { ExplicitFee = network.MinFee }; - psbt = (await nbx.CreatePSBTAsync(derivationScheme, psbtRequest, normalOperationTimeout.Token)).PSBT; + psbt = (await nbx.CreatePSBTAsync(derivationScheme, psbtRequest, normalOperationTimeout.Token)); } } @@ -538,7 +538,7 @@ namespace BTCPayServer.Controllers // Here we rebase the hd_keys in the PSBT to have a keypath relative to the root HD so the wallet can sign // Note that the fingerprint of the hd keys are now 0, which is wrong // However, hardware wallets does not give a damn, and sometimes does not even allow us to get this fingerprint anyway. - foreach (var o in psbt.Inputs.OfType().Concat(psbt.Outputs)) + foreach (var o in psbt.PSBT.Inputs.OfType().Concat(psbt.PSBT.Outputs)) { foreach (var keypath in o.HDKeyPaths.ToList()) { @@ -549,12 +549,12 @@ namespace BTCPayServer.Controllers } signTimeout.CancelAfter(TimeSpan.FromMinutes(5)); - psbt = await hw.SignTransactionAsync(psbt, signTimeout.Token); - if(!psbt.TryFinalize(out var errors)) + psbt.PSBT = await hw.SignTransactionAsync(psbt.PSBT, psbt.ChangeAddress?.ScriptPubKey, signTimeout.Token); + if(!psbt.PSBT.TryFinalize(out var errors)) { throw new Exception($"Error while finalizing the transaction ({new PSBTException(errors).ToString()})"); } - var transaction = psbt.ExtractTransaction(); + var transaction = psbt.PSBT.ExtractTransaction(); try { var broadcastResult = await nbx.BroadcastAsync(transaction); diff --git a/BTCPayServer/Services/HardwareWalletService.cs b/BTCPayServer/Services/HardwareWalletService.cs index 08d19dc26..535ccc1ac 100644 --- a/BTCPayServer/Services/HardwareWalletService.cs +++ b/BTCPayServer/Services/HardwareWalletService.cs @@ -164,13 +164,15 @@ namespace BTCPayServer.Services return foundKeyPath; } - public async Task SignTransactionAsync(PSBT psbt, + public async Task SignTransactionAsync(PSBT psbt, Script changeHint, CancellationToken cancellationToken) { try { var unsigned = psbt.GetGlobalTransaction(); - var changeKeyPath = psbt.Outputs.Where(o => o.HDKeyPaths.Any()) + var changeKeyPath = psbt.Outputs + .Where(o => changeHint == null ? true : changeHint == o.ScriptPubKey) + .Where(o => o.HDKeyPaths.Any()) .Select(o => o.HDKeyPaths.First().Value.Item2) .FirstOrDefault(); var signatureRequests = psbt From f93d1173e29138c0a03a58f1a9a89ac694f4e130 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 7 May 2019 13:58:55 +0900 Subject: [PATCH 020/243] Show tor exposed bitcoin node --- BTCPayServer/BTCPayServer.csproj | 3 + .../Configuration/ExternalConnectionString.cs | 8 +++ BTCPayServer/Controllers/ServerController.cs | 43 ++++++++++- .../HostedServices/NBXplorerWaiter.cs | 2 +- BTCPayServer/Services/TorServices.cs | 21 +++++- BTCPayServer/Views/Server/P2PService.cshtml | 72 +++++++++++++++++++ 6 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 BTCPayServer/Views/Server/P2PService.cshtml diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 1b6e29003..9c23c5488 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -151,6 +151,9 @@ $(IncludeRazorContentInPack) + + $(IncludeRazorContentInPack) + $(IncludeRazorContentInPack) diff --git a/BTCPayServer/Configuration/ExternalConnectionString.cs b/BTCPayServer/Configuration/ExternalConnectionString.cs index fe4e7990c..08b1ee9b1 100644 --- a/BTCPayServer/Configuration/ExternalConnectionString.cs +++ b/BTCPayServer/Configuration/ExternalConnectionString.cs @@ -8,6 +8,14 @@ namespace BTCPayServer.Configuration { public class ExternalConnectionString { + public ExternalConnectionString() + { + + } + public ExternalConnectionString(Uri server) + { + Server = server; + } public Uri Server { get; set; } public byte[] Macaroon { get; set; } public Macaroons Macaroons { get; set; } diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index d9d01cb43..86146b6fe 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -532,6 +532,10 @@ namespace BTCPayServer.Controllers Link = $"http://{torService.OnionHost}" }); } + else if (TryParseAsExternalService(torService, out var externalService)) + { + result.ExternalServices.Add(externalService); + } else { result.TorOtherServices.Add(new ServicesViewModel.OtherExternalService() @@ -551,6 +555,32 @@ namespace BTCPayServer.Controllers return View(result); } + private static bool TryParseAsExternalService(TorService torService, out ExternalService externalService) + { + externalService = null; + if (torService.ServiceType == TorServiceType.P2P) + { + externalService = new ExternalService() + { + CryptoCode = torService.Network.CryptoCode, + DisplayName = "Full node P2P", + Type = ExternalServiceTypes.P2P, + ConnectionString = new ExternalConnectionString(new Uri($"bitcoin-p2p://{torService.OnionHost}:{torService.VirtualPort}", UriKind.Absolute)), + ServiceName = torService.Name, + }; + } + return externalService != null; + } + + private ExternalService GetService(string serviceName, string cryptoCode) + { + var result = _Options.ExternalServices.GetService(serviceName, cryptoCode); + if (result != null) + return result; + _torServices.Services.FirstOrDefault(s => TryParseAsExternalService(s, out result)); + return result; + } + [Route("server/services/{serviceName}/{cryptoCode}")] public async Task Service(string serviceName, string cryptoCode, bool showQR = false, uint? nonce = null) { @@ -559,12 +589,21 @@ namespace BTCPayServer.Controllers StatusMessage = $"Error: {cryptoCode} is not fully synched"; return RedirectToAction(nameof(Services)); } - var service = _Options.ExternalServices.GetService(serviceName, cryptoCode); + var service = GetService(serviceName, cryptoCode); if (service == null) return NotFound(); try { + if (service.Type == ExternalServiceTypes.P2P) + { + return View("P2PService", new LightningWalletServices() + { + ShowQR = showQR, + WalletName = service.ServiceName, + ServiceLink = service.ConnectionString.Server.AbsoluteUri + }); + } var connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type); switch (service.Type) { @@ -674,7 +713,7 @@ namespace BTCPayServer.Controllers StatusMessage = $"Error: {cryptoCode} is not fully synched"; return RedirectToAction(nameof(Services)); } - var service = _Options.ExternalServices.GetService(serviceName, cryptoCode); + var service = GetService(serviceName, cryptoCode); if (service == null) return NotFound(); diff --git a/BTCPayServer/HostedServices/NBXplorerWaiter.cs b/BTCPayServer/HostedServices/NBXplorerWaiter.cs index f1308b421..1fe07bb6e 100644 --- a/BTCPayServer/HostedServices/NBXplorerWaiter.cs +++ b/BTCPayServer/HostedServices/NBXplorerWaiter.cs @@ -49,7 +49,7 @@ namespace BTCPayServer.HostedServices } public NBXplorerSummary Get(string cryptoCode) { - _Summaries.TryGetValue(cryptoCode, out var summary); + _Summaries.TryGetValue(cryptoCode.ToUpperInvariant(), out var summary); return summary; } public IEnumerable GetAll() diff --git a/BTCPayServer/Services/TorServices.cs b/BTCPayServer/Services/TorServices.cs index 11487d462..39a771ffb 100644 --- a/BTCPayServer/Services/TorServices.cs +++ b/BTCPayServer/Services/TorServices.cs @@ -11,9 +11,11 @@ namespace BTCPayServer.Services { public class TorServices { + private readonly BTCPayNetworkProvider _networks; BTCPayServerOptions _Options; - public TorServices(BTCPayServerOptions options) + public TorServices(BTCPayServer.BTCPayNetworkProvider networks, BTCPayServerOptions options) { + _networks = networks; _Options = options; } @@ -58,6 +60,11 @@ namespace BTCPayServer.Services }; if (service.ServiceName.Equals("BTCPayServer", StringComparison.OrdinalIgnoreCase)) torService.ServiceType = TorServiceType.BTCPayServer; + else if (TryParseP2PService(service.ServiceName, out var network)) + { + torService.ServiceType = TorServiceType.P2P; + torService.Network = network; + } result.Add(torService); } catch (Exception ex) @@ -72,11 +79,22 @@ namespace BTCPayServer.Services } Services = result.ToArray(); } + + private bool TryParseP2PService(string name, out BTCPayNetwork network) + { + network = null; + var splitted = name.Trim().Split('-'); + if (splitted.Length != 2 || splitted[1] != "P2P") + return false; + network = _networks.GetNetwork(splitted[0]); + return network != null; + } } public class TorService { public TorServiceType ServiceType { get; set; } = TorServiceType.Other; + public BTCPayNetwork Network { get; set; } public string Name { get; set; } public string OnionHost { get; set; } public int VirtualPort { get; set; } @@ -85,6 +103,7 @@ namespace BTCPayServer.Services public enum TorServiceType { BTCPayServer, + P2P, Other } } diff --git a/BTCPayServer/Views/Server/P2PService.cshtml b/BTCPayServer/Views/Server/P2PService.cshtml new file mode 100644 index 000000000..768b0198f --- /dev/null +++ b/BTCPayServer/Views/Server/P2PService.cshtml @@ -0,0 +1,72 @@ +@model LightningWalletServices +@{ + ViewData.SetActivePageAndTitle(ServerNavPages.Services); +} + + +

@Model.WalletName

+ + +@if (Model.ShowQR) +{ + +} + +
+
+
+
+
+ +
+ +
+
+
QR Code connection
+

+ You can use QR Code to connect to your @Model.WalletName from your mobile.
+

+
+
+ @if (!Model.ShowQR) + { +
+
+ + +
+
+ } + else + { +
+
+
+
+ } +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") + + @if (Model.ShowQR) + { + + + } +} From bf035333cf98c0bf7aee5399a64401a9bc83bc04 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 7 May 2019 14:07:36 +0900 Subject: [PATCH 021/243] Add service type P2P --- BTCPayServer/Configuration/ExternalService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BTCPayServer/Configuration/ExternalService.cs b/BTCPayServer/Configuration/ExternalService.cs index bfd3688b4..0e56af98d 100644 --- a/BTCPayServer/Configuration/ExternalService.cs +++ b/BTCPayServer/Configuration/ExternalService.cs @@ -75,6 +75,7 @@ namespace BTCPayServer.Configuration LNDGRPC, Spark, RTL, - Charge + Charge, + P2P } } From 5967666df66f351d081eef29b288188514e5fabd Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 7 May 2019 14:16:44 +0900 Subject: [PATCH 022/243] Add green wallet info --- BTCPayServer/Views/Server/P2PService.cshtml | 14 ++++++++++++-- BTCPayServer/wwwroot/img/GreenWallet.png | Bin 0 -> 2638 bytes 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 BTCPayServer/wwwroot/img/GreenWallet.png diff --git a/BTCPayServer/Views/Server/P2PService.cshtml b/BTCPayServer/Views/Server/P2PService.cshtml index 768b0198f..16644481f 100644 --- a/BTCPayServer/Views/Server/P2PService.cshtml +++ b/BTCPayServer/Views/Server/P2PService.cshtml @@ -25,14 +25,24 @@
-
QR Code connection

- You can use QR Code to connect to your @Model.WalletName from your mobile.
+ You can use QR Code to connect @Model.WalletName from outside application.

+
+
Compatible wallets
+
+
@if (!Model.ShowQR) { diff --git a/BTCPayServer/wwwroot/img/GreenWallet.png b/BTCPayServer/wwwroot/img/GreenWallet.png new file mode 100644 index 0000000000000000000000000000000000000000..61efbefa49e91a97544ff2e7a0c0bfc2b998d68c GIT binary patch literal 2638 zcmd_qi$9cE9{})^c`z3<&tu$YkeHz>g>jlNOobIRVUi{mq2Lv%crq16s0z}krJ$q-o0yo`*!cJ#Qc_aov-is9I?7+*V$<2U40|l!9(&dS zo4pr%ey?1ClbnDjSICnqa+bT~B3tAld)XB#c9p&628s4TrS6z=Pnlo5pr1UTpFN>+ zPt45&GF1m;st<0u<-Mua2Q>JBJO1Ezf6y2JngYNdhd^^6XgLI00zq3)T3UK~Mkb%n z&&tZm&d$!w&CSoxFOU)l1cgGOu&Ai0xVZS5ln7N)A{L8F%gV~GOO@ZaQGu$gtgNoC zuBoZ{t+uwV?)L5a`Ua^xjZ#gGO;UHI{%CG)Zf$Gp=;-LYkLv2`?(XhE^-A^i_4f}9 z4F1p1gW=)Fk4HvEo;)2L9UB`PADbswf5530e9j<#!d$4j&Cl>wb@|zTT?SVWW%F)OL@Jy^C*zSfy+fRiGkWCA-%r5rF4H;tr=77NadD~HdTNp2 z$-1Fpl^rvs_33A^du-5_nFc%Ky5+wgofadlMg{IzntD3)DwXb`8q4l#6?26{Vvjjd zmhm%EoS&li^)-ng>F!UxALJ?o#dUF{#s0i@VUZ6n_|rX}js%@cz05gT;TU?vPNjw1 zOiQy^a#labNSeBo*4Xy7Td6Ho0~#F1G~VYu(wjTPt!~$>xk;cE?8#8%GG%b*9Le_# zlZW;1Tu|qI;9e&!>rz|t%arqJHnI|J$fn!(iDCYvLPn(-;OV`hC8{87>b^USQK!p<1{cvS6uCQ)+;mv~cZ&mXh zG$Fmng94qB$IxrWG_NJqxm`A$o6n5nb_i8n>>{ipzMp@6m|5Mkr~gII@tmR7n6-UV z$ZoGtNq(`zv`TNM8wvVV!pt6KZ%lyY3uhZyIh-Dr)7ao9VRa{ymvBe&<8v}5q20_R zC~aL9QaTZ$BIneQYyR*}5%T13-h3c*cPxaxvn7vZvYA5cHDbSEre^ZF?qp0|&;H)HOU#|A}dPi^4ztuGm=9fbp5D~Mj zPpQQ*8qyBbf`80kb>VYz^&+_ip(|P!8EfV?DGlJ4h{epY@7(oh^IW9K$+Yi&k7$!UEeZ;XLpYc?} z{F5Wzy0HE+?~<+SUNTq4azZJCc$__br zE%l$mj9=DarVuL@r|O5is-V28&KYe=PnT9tI+UJWXW<{Ijq&9jHE|i;wNs>>s%}7r zULJLMVV^{dbV%Z}-}?<#7MwK7@z8X7O{cU7n*u2KpCs)eNcWW~uANo$=ywBKZrmaV zhfq^y%YL>xx$ri2D|?@saknz(58knV|IirnmrS0bi1+aJnT(S;X5|rv!yu3T-CIV` zw0lK$t@VlCrUGoVOinCQNmb!xl1a=Kb}!Pz`aQAI=TKx@SVSzHoluCI%fb@}-0s+9 z>Mqi-8h~(Q8FwW$5UgjJHIn0dTG&Y+g%O6V-4gYees+lszrO3P6Vy;@Ki!{C=X=?d zj)+*JE}5b8)13rdA`q8fE!`PTo6d$bO0dRLpHc@t1*hbhN~kz|z;e~=_x5#}xnie> zUwlW#*S7_gK4qdr@Z)829uB?n_%d6lAV;Yz`EVb97kaG& NCkHo9ja_iYe*r@>;&%W5 literal 0 HcmV?d00001 From b6c37a73b125053ecf09685ca89ecbc7589ad2e6 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 7 May 2019 14:31:49 +0900 Subject: [PATCH 023/243] Fix duplicated entries on Services. Fix formatting of P2P page. --- BTCPayServer/Controllers/ServerController.cs | 2 +- BTCPayServer/Views/Server/P2PService.cshtml | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index 86146b6fe..80fa8b11f 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -505,7 +505,7 @@ namespace BTCPayServer.Controllers public async Task Services() { var result = new ServicesViewModel(); - result.ExternalServices = _Options.ExternalServices; + result.ExternalServices = _Options.ExternalServices.ToList(); foreach (var externalService in _Options.OtherExternalServices) { result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService() diff --git a/BTCPayServer/Views/Server/P2PService.cshtml b/BTCPayServer/Views/Server/P2PService.cshtml index 16644481f..92bbc0c2c 100644 --- a/BTCPayServer/Views/Server/P2PService.cshtml +++ b/BTCPayServer/Views/Server/P2PService.cshtml @@ -29,7 +29,7 @@
QR Code connection

- You can use QR Code to connect @Model.WalletName from outside application.
+ You can use QR Code to connect to @Model.WalletName with compatible wallets.

@@ -42,6 +42,15 @@

Blockstream Green Wallet

+
+ +
+
+ +
+
+ +
@if (!Model.ShowQR) @@ -59,6 +68,13 @@
+

See QR Code information by clicking here

+
+
+ + +
+
}
From 232ceed8b008c443ed3e99ce5240f651c14accad Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 7 May 2019 14:44:26 +0900 Subject: [PATCH 024/243] Prettify the bitcoin core node page --- BTCPayServer/Controllers/ServerController.cs | 2 +- BTCPayServer/Extensions.cs | 6 ++++++ BTCPayServer/Views/Server/P2PService.cshtml | 10 ++++++++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index 80fa8b11f..4f5da9766 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -601,7 +601,7 @@ namespace BTCPayServer.Controllers { ShowQR = showQR, WalletName = service.ServiceName, - ServiceLink = service.ConnectionString.Server.AbsoluteUri + ServiceLink = service.ConnectionString.Server.AbsoluteUri.WithoutEndingSlash() }); } var connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type); diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 91168c85f..b6dfe50de 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -132,6 +132,12 @@ namespace BTCPayServer return str; return $"/{str}"; } + public static string WithoutEndingSlash(this string str) + { + if (str.EndsWith("/", StringComparison.InvariantCulture)) + return str.Substring(0, str.Length - 1); + return str; + } public static void SetHeaderOnStarting(this HttpResponse resp, string name, string value) { diff --git a/BTCPayServer/Views/Server/P2PService.cshtml b/BTCPayServer/Views/Server/P2PService.cshtml index 92bbc0c2c..95e655f11 100644 --- a/BTCPayServer/Views/Server/P2PService.cshtml +++ b/BTCPayServer/Views/Server/P2PService.cshtml @@ -27,9 +27,9 @@
-
QR Code connection
+
Full node connection

- You can use QR Code to connect to @Model.WalletName with compatible wallets.
+ This page exposes information to connect remotely to your full node via the P2P protocol.

@@ -52,6 +52,12 @@
+
+
QR Code connection
+

+ You can use QR Code to connect to @Model.WalletName with compatible wallets.
+

+
@if (!Model.ShowQR) { From 50351f56f81097749f0c02c5cba2cbc9565fbada Mon Sep 17 00:00:00 2001 From: Lucas Cullen Date: Tue, 7 May 2019 17:55:22 +1000 Subject: [PATCH 025/243] Fix grammar (#817) * Fix grammar * Fix typo --- BTCPayServer/Views/Shared/_Layout.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/Views/Shared/_Layout.cshtml b/BTCPayServer/Views/Shared/_Layout.cshtml index e285b566e..4c984dbaf 100644 --- a/BTCPayServer/Views/Shared/_Layout.cshtml +++ b/BTCPayServer/Views/Shared/_Layout.cshtml @@ -104,7 +104,7 @@ { } From 0936812df071d9242a3eaa413aa6bb5ffe42872f Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Tue, 7 May 2019 08:01:37 +0000 Subject: [PATCH 026/243] Fix date time issues on crowdfund.payment requests (#808) * fix some conditional display bugs in crowdfund * bump flatpickr * make clear button show up even with flatpickt fake input ui * update uis to specify date value in specific format and use custom format for flatpickr display and use moment to parse date instead * fix remaining public ui date issues --- .../Views/Apps/UpdateCrowdfund.cshtml | 12 +++++- .../AppsPublic/Crowdfund/VueCrowdfund.cshtml | 42 +++++++++---------- .../PaymentRequest/EditPaymentRequest.cshtml | 8 +++- BTCPayServer/wwwroot/crowdfund-admin/main.js | 13 ++++-- BTCPayServer/wwwroot/crowdfund/app.js | 10 +++-- .../wwwroot/crowdfund/styles/main.css | 4 ++ BTCPayServer/wwwroot/main/site.js | 14 ++++--- .../wwwroot/payment-request-admin/main.js | 13 ++++-- BTCPayServer/wwwroot/payment-request/app.js | 4 +- .../wwwroot/vendor/flatpickr/flatpickr.js | 4 +- .../vendor/flatpickr/flatpickr.min.css | 2 +- 11 files changed, 81 insertions(+), 45 deletions(-) diff --git a/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml b/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml index 9986b3bf8..dd6acdb04 100644 --- a/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml +++ b/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml @@ -1,4 +1,5 @@ @addTagHelper *, BundlerMinifier.TagHelpers +@using System.Globalization @model UpdateCrowdfundViewModel @{ ViewData["Title"] = "Update Crowdfund"; @@ -67,7 +68,9 @@
- +
-
+
{{ raisedAmount }} {{targetCurrency}}
Raised
@@ -73,23 +73,7 @@
Contributors
-
-
- {{endDiff}} -
-
Left
- -
    -
  • - {{started? "Started" : "Starts"}} {{startDate}} -
  • -
  • - {{ended? "Ended" : "Ends"}} {{endDate}} -
  • -
-
-
-
+
{{startDiff}}
@@ -106,7 +90,23 @@
-
+
+
+ {{endDiff}} +
+
Left
+ +
    +
  • + {{started? "Started" : "Starts"}} {{startDate}} +
  • +
  • + {{ended? "Ended" : "Ends"}} {{endDate}} +
  • +
+
+
+
Campaign
diff --git a/BTCPayServer/Views/PaymentRequest/EditPaymentRequest.cshtml b/BTCPayServer/Views/PaymentRequest/EditPaymentRequest.cshtml index fcc5f1576..01f6a212e 100644 --- a/BTCPayServer/Views/PaymentRequest/EditPaymentRequest.cshtml +++ b/BTCPayServer/Views/PaymentRequest/EditPaymentRequest.cshtml @@ -1,4 +1,5 @@ @using BTCPayServer.Services.PaymentRequests +@using System.Globalization @model BTCPayServer.Models.PaymentRequestViewModels.UpdatePaymentRequestViewModel @addTagHelper *, BundlerMinifier.TagHelpers @@ -66,7 +67,10 @@
- +