diff --git a/BTCPayServer.Tests/ServerTester.cs b/BTCPayServer.Tests/ServerTester.cs index 18870ac75..1dbd3c55c 100644 --- a/BTCPayServer.Tests/ServerTester.cs +++ b/BTCPayServer.Tests/ServerTester.cs @@ -51,7 +51,7 @@ namespace BTCPayServer.Tests return String.IsNullOrEmpty(var) ? defaultValue : var; } - public TestAccount CreateAccount() + public TestAccount NewAccount() { return new TestAccount(this); } diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index 730570906..b7b2c8804 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -26,10 +26,45 @@ namespace BTCPayServer.Tests { GrantAccessAsync().GetAwaiter().GetResult(); } + + public void Register() + { + RegisterAsync().GetAwaiter().GetResult(); + } + + public BitcoinExtKey ExtKey + { + get; set; + } + public async Task GrantAccessAsync() { - var extKey = new ExtKey().GetWif(parent.Network); + await RegisterAsync(); + var store = await CreateStoreAsync(); var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant); + Assert.IsType(await store.RequestPairing(pairingCode.ToString())); + await store.Pair(pairingCode.ToString(), StoreId); + } + public StoresController CreateStore() + { + return CreateStoreAsync().GetAwaiter().GetResult(); + } + public async Task CreateStoreAsync() + { + ExtKey = new ExtKey().GetWif(parent.Network); + var store = parent.PayTester.GetController(UserId); + await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" }); + StoreId = store.CreatedStoreId; + await store.UpdateStore(StoreId, new StoreViewModel() + { + DerivationScheme = ExtKey.Neuter().ToString() + "-[legacy]", + SpeedPolicy = SpeedPolicy.MediumSpeed + }, "Save"); + return store; + } + + private async Task RegisterAsync() + { var account = parent.PayTester.GetController(); await account.Register(new RegisterViewModel() { @@ -38,18 +73,6 @@ namespace BTCPayServer.Tests Password = "Kitten0@", }); UserId = account.RegisteredUserId; - - var store = parent.PayTester.GetController(account.RegisteredUserId); - await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" }); - StoreId = store.CreatedStoreId; - - await store.UpdateStore(StoreId, new StoreViewModel() - { - DerivationScheme = extKey.Neuter().ToString() + "-[legacy]", - SpeedPolicy = SpeedPolicy.MediumSpeed - }, "Save"); - Assert.IsType(await store.RequestPairing(pairingCode.ToString())); - await store.Pair(pairingCode.ToString(), StoreId); } public Bitpay BitPay diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 46d2384dd..818618ed1 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -13,6 +13,8 @@ using BTCPayServer.Servcices.Invoices; using Newtonsoft.Json; using System.IO; using Newtonsoft.Json.Linq; +using BTCPayServer.Controllers; +using Microsoft.AspNetCore.Mvc; namespace BTCPayServer.Tests { @@ -63,7 +65,7 @@ namespace BTCPayServer.Tests using(var tester = ServerTester.Create()) { tester.Start(); - var user = tester.CreateAccount(); + var user = tester.NewAccount(); user.GrantAccess(); var invoice = user.BitPay.CreateInvoice(new Invoice() { @@ -104,6 +106,31 @@ namespace BTCPayServer.Tests } } + [Fact] + public void CanUseServerInitiatedPairingCode() + { + using(var tester = ServerTester.Create()) + { + tester.Start(); + var acc = tester.NewAccount(); + acc.Register(); + acc.CreateStore(); + + var controller = tester.PayTester.GetController(acc.UserId); + var token = (RedirectToActionResult)controller.CreateToken(acc.StoreId, new Models.StoreViewModels.CreateTokenViewModel() + { + Facade = Facade.Merchant.ToString(), + Label = "bla", + PublicKey = null + }).GetAwaiter().GetResult(); + + var pairingCode = (string)token.RouteValues["pairingCode"]; + + acc.BitPay.AuthorizeClient(new PairingCode(pairingCode)).GetAwaiter().GetResult(); + Assert.True(acc.BitPay.TestAccess(Facade.Merchant)); + } + } + [Fact] public void CanSendIPN() { @@ -112,7 +139,7 @@ namespace BTCPayServer.Tests using(var tester = ServerTester.Create()) { tester.Start(); - var acc = tester.CreateAccount(); + var acc = tester.NewAccount(); acc.GrantAccess(); var invoice = acc.BitPay.CreateInvoice(new Invoice() { @@ -143,7 +170,7 @@ namespace BTCPayServer.Tests using(var tester = ServerTester.Create()) { tester.Start(); - var user = tester.CreateAccount(); + var user = tester.NewAccount(); Assert.False(user.BitPay.TestAccess(Facade.Merchant)); user.GrantAccess(); Assert.True(user.BitPay.TestAccess(Facade.Merchant)); diff --git a/BTCPayServer/Authentication/BitToken.cs b/BTCPayServer/Authentication/BitToken.cs index be06afae6..e933801db 100644 --- a/BTCPayServer/Authentication/BitToken.cs +++ b/BTCPayServer/Authentication/BitToken.cs @@ -8,7 +8,7 @@ namespace BTCPayServer.Authentication { public class BitTokenEntity { - public string Name + public string Facade { get; set; } @@ -16,15 +16,7 @@ namespace BTCPayServer.Authentication { get; set; } - public DateTimeOffset DateCreated - { - get; set; - } - public bool Active - { - get; set; - } - public string PairedId + public string StoreId { get; set; } @@ -46,11 +38,9 @@ namespace BTCPayServer.Authentication { return new BitTokenEntity() { - Active = Active, - DateCreated = DateCreated, Label = Label, - Name = Name, - PairedId = PairedId, + Facade = Facade, + StoreId = StoreId, PairingTime = PairingTime, SIN = SIN, Value = Value diff --git a/BTCPayServer/Authentication/PairingCodeEntity.cs b/BTCPayServer/Authentication/PairingCodeEntity.cs index 9b202969c..0afbf8958 100644 --- a/BTCPayServer/Authentication/PairingCodeEntity.cs +++ b/BTCPayServer/Authentication/PairingCodeEntity.cs @@ -26,17 +26,17 @@ namespace BTCPayServer.Authentication get; set; } - public DateTimeOffset PairingTime + public DateTimeOffset CreatedTime { get; set; } - public DateTimeOffset PairingExpiration + public DateTimeOffset Expiration { get; set; } - public string Token + public string TokenValue { get; set; @@ -44,7 +44,7 @@ namespace BTCPayServer.Authentication public bool IsExpired() { - return DateTimeOffset.UtcNow > PairingExpiration; + return DateTimeOffset.UtcNow > Expiration; } } } diff --git a/BTCPayServer/Authentication/TokenRepository.cs b/BTCPayServer/Authentication/TokenRepository.cs index 79952674c..b06696d52 100644 --- a/BTCPayServer/Authentication/TokenRepository.cs +++ b/BTCPayServer/Authentication/TokenRepository.cs @@ -1,4 +1,5 @@ -using DBreeze; +using BTCPayServer.Data; +using DBreeze; using NBitcoin; using NBitcoin.DataEncoders; using Newtonsoft.Json; @@ -6,175 +7,177 @@ using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; +using System.Linq; namespace BTCPayServer.Authentication { public class TokenRepository { - public TokenRepository(DBreezeEngine engine) + ApplicationDbContextFactory _Factory; + public TokenRepository(ApplicationDbContextFactory dbFactory) { - _Engine = engine; + if(dbFactory == null) + throw new ArgumentNullException(nameof(dbFactory)); + _Factory = dbFactory; } - - private readonly DBreezeEngine _Engine; - public DBreezeEngine Engine + public async Task GetTokens(string sin) { - get + using(var ctx = _Factory.CreateContext()) { - return _Engine; + return (await ctx.PairedSINData + .Where(p => p.SIN == sin) + .ToListAsync()) + .Select(p => CreateTokenEntity(p)) + .ToArray(); } } - public Task GetTokens(string sin) + private BitTokenEntity CreateTokenEntity(PairedSINData data) { - List tokens = new List(); - using(var tx = _Engine.GetTransaction()) + return new BitTokenEntity() { - tx.ValuesLazyLoadingIsOn = false; - foreach(var row in tx.SelectForward($"T_{sin}")) - { - var token = ToObject(row.Value); - tokens.Add(token); - } - } - return Task.FromResult(tokens.ToArray()); - } - - public Task CreateToken(string sin, string tokenName) - { - var token = new BitTokenEntity - { - Name = tokenName, - Value = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32)), - DateCreated = DateTimeOffset.UtcNow + Label = data.Label, + Facade = data.Facade, + Value = data.Id, + SIN = data.SIN, + PairingTime = data.PairingTime, + StoreId = data.StoreDataId }; - using(var tx = _Engine.GetTransaction()) - { - tx.Insert($"T_{sin}", token.Name, ToBytes(token)); - tx.Commit(); - } - return Task.FromResult(token); } - public Task PairWithAsync(string pairingCode, string pairedId) + public async Task CreatePairingCodeAsync() { - if(pairedId == null) - throw new ArgumentNullException(nameof(pairedId)); - using(var tx = _Engine.GetTransaction()) + string pairingCodeId = Encoders.Base58.EncodeData(RandomUtils.GetBytes(6)); + using(var ctx = _Factory.CreateContext()) { - var row = tx.Select("PairingCodes", pairingCode); - if(row == null || !row.Exists) - return Task.FromResult(false); - tx.RemoveKey("PairingCodes", pairingCode); - try + var now = DateTime.UtcNow; + var expiration = DateTime.UtcNow + TimeSpan.FromMinutes(15); + await ctx.PairingCodes.AddAsync(new PairingCodeData() { - var pairingEntity = ToObject(row.Value); - if(pairingEntity.IsExpired()) - return Task.FromResult(false); - row = tx.Select($"T_{pairingEntity.SIN}", pairingEntity.Facade); - if(row == null || !row.Exists) - return Task.FromResult(false); - var token = ToObject(row.Value); - if(token.Active) - return Task.FromResult(false); - token.Active = true; - token.PairedId = pairedId; - token.SIN = pairingEntity.SIN; - token.Label = pairingEntity.Label; - token.PairingTime = DateTimeOffset.UtcNow; - tx.Insert($"TbP_{pairedId}", token.Value, ToBytes(token)); - tx.Insert($"T_{pairingEntity.SIN}", pairingEntity.Facade, ToBytes(token)); - } - finally - { - tx.Commit(); - } + Id = pairingCodeId, + DateCreated = now, + Expiration = expiration, + TokenValue = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32)) + }); + await ctx.SaveChangesAsync(); } - return Task.FromResult(true); + return pairingCodeId; } - public Task GetTokensByPairedIdAsync(string pairedId) + public async Task UpdatePairingCode(PairingCodeEntity pairingCodeEntity) { - List tokens = new List(); - using(var tx = _Engine.GetTransaction()) + using(var ctx = _Factory.CreateContext()) { - tx.ValuesLazyLoadingIsOn = false; - foreach(var row in tx.SelectForward($"TbP_{pairedId}")) - { - tokens.Add(ToObject(row.Value)); - } - } - return Task.FromResult(tokens.ToArray()); - } - - public Task GetPairingAsync(string pairingCode) - { - using(var tx = _Engine.GetTransaction()) - { - var row = tx.Select("PairingCodes", pairingCode); - if(row == null || !row.Exists) - return Task.FromResult(null); - var pairingEntity = ToObject(row.Value); - if(pairingEntity.IsExpired()) - return Task.FromResult(null); - return Task.FromResult(pairingEntity); + var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeEntity.Id); + pairingCode.Label = pairingCodeEntity.Label; + pairingCode.Facade = pairingCodeEntity.Facade; + await ctx.SaveChangesAsync(); + return CreatePairingCodeEntity(pairingCode); } } - public Task AddPairingCodeAsync(PairingCodeEntity pairingCodeEntity) + + public async Task PairWithStoreAsync(string pairingCodeId, string storeId) { - pairingCodeEntity = Clone(pairingCodeEntity); - pairingCodeEntity.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(6)); - using(var tx = _Engine.GetTransaction()) + using(var ctx = _Factory.CreateContext()) { - tx.Insert("PairingCodes", pairingCodeEntity.Id, ToBytes(pairingCodeEntity)); - tx.Commit(); - } - return Task.FromResult(pairingCodeEntity); - } - - private byte[] ToBytes(T obj) - { - return ZipUtils.Zip(JsonConvert.SerializeObject(obj)); - } - private T ToObject(byte[] value) - { - return JsonConvert.DeserializeObject(ZipUtils.Unzip(value)); - } - - private T Clone(T obj) - { - return ToObject(ToBytes(obj)); - } - - - public async Task DeleteToken(string sin, string tokenName, string storeId) - { - var token = await GetToken(sin, tokenName); - if(token == null || (token.PairedId != null && token.PairedId != storeId)) - return false; - using(var tx = _Engine.GetTransaction()) - { - tx.RemoveKey($"T_{sin}", tokenName); - if(token.PairedId != null) - tx.RemoveKey($"TbP_" + token.PairedId, token.Value); - tx.Commit(); + var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId); + if(pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow) + return false; + pairingCode.StoreDataId = storeId; + await ActivateIfComplete(ctx, pairingCode); + await ctx.SaveChangesAsync(); } return true; } - private Task GetToken(string sin, string tokenName) + public async Task PairWithSINAsync(string pairingCodeId, string sin) { - using(var tx = _Engine.GetTransaction()) + using(var ctx = _Factory.CreateContext()) { - tx.ValuesLazyLoadingIsOn = true; - var row = tx.Select($"T_{sin}", tokenName); - if(row == null || !row.Exists) - return Task.FromResult(null); - var token = ToObject(row.Value); - if(!token.Active) - return Task.FromResult(null); - return Task.FromResult(token); + var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId); + if(pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow) + return false; + pairingCode.SIN = sin; + await ActivateIfComplete(ctx, pairingCode); + await ctx.SaveChangesAsync(); + } + return true; + } + + + private async Task ActivateIfComplete(ApplicationDbContext ctx, PairingCodeData pairingCode) + { + if(!string.IsNullOrEmpty(pairingCode.SIN) && !string.IsNullOrEmpty(pairingCode.StoreDataId)) + { + ctx.PairingCodes.Remove(pairingCode); + await ctx.PairedSINData.AddAsync(new PairedSINData() + { + Id = pairingCode.TokenValue, + PairingTime = DateTime.UtcNow, + Facade = pairingCode.Facade, + Label = pairingCode.Label, + StoreDataId = pairingCode.StoreDataId, + SIN = pairingCode.SIN + }); + } + } + + + public async Task GetTokensByStoreIdAsync(string storeId) + { + using(var ctx = _Factory.CreateContext()) + { + return (await ctx.PairedSINData.Where(p => p.StoreDataId == storeId).ToListAsync()) + .Select(c => CreateTokenEntity(c)) + .ToArray(); + } + } + + public async Task GetPairingAsync(string pairingCode) + { + using(var ctx = _Factory.CreateContext()) + { + return CreatePairingCodeEntity(await ctx.PairingCodes.FindAsync(pairingCode)); + } + } + + private PairingCodeEntity CreatePairingCodeEntity(PairingCodeData data) + { + return new PairingCodeEntity() + { + Facade = data.Facade, + Id = data.Id, + Label = data.Label, + Expiration = data.Expiration, + CreatedTime = data.DateCreated, + TokenValue = data.TokenValue, + SIN = data.SIN + }; + } + + + public async Task DeleteToken(string tokenId) + { + using(var ctx = _Factory.CreateContext()) + { + var token = await ctx.PairedSINData.FindAsync(tokenId); + if(token == null) + return false; + ctx.PairedSINData.Remove(token); + await ctx.SaveChangesAsync(); + return true; + } + } + + public async Task GetToken(string tokenId) + { + using(var ctx = _Factory.CreateContext()) + { + var token = await ctx.PairedSINData.FindAsync(tokenId); + return CreateTokenEntity(token); } } diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index af9c4bc0d..9e98b2f30 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -2,7 +2,7 @@ Exe netcoreapp2.0 - 1.0.0.5 + 1.0.0.6 @@ -22,7 +22,7 @@ - + diff --git a/BTCPayServer/Configuration/BTCPayServerRuntime.cs b/BTCPayServer/Configuration/BTCPayServerRuntime.cs index 7de3f8a62..568b266e2 100644 --- a/BTCPayServer/Configuration/BTCPayServerRuntime.cs +++ b/BTCPayServer/Configuration/BTCPayServerRuntime.cs @@ -51,7 +51,6 @@ namespace BTCPayServer.Configuration } DBreezeEngine db = new DBreezeEngine(CreateDBPath(opts, "TokensDB")); _Resources.Add(db); - TokenRepository = new TokenRepository(db); db = new DBreezeEngine(CreateDBPath(opts, "InvoiceDB")); _Resources.Add(db); @@ -99,10 +98,6 @@ namespace BTCPayServer.Configuration get; private set; } - public TokenRepository TokenRepository - { - get; set; - } public InvoiceRepository InvoiceRepository { get; diff --git a/BTCPayServer/Controllers/AccessTokenController.cs b/BTCPayServer/Controllers/AccessTokenController.cs index f5bfc2df0..2afa5389e 100644 --- a/BTCPayServer/Controllers/AccessTokenController.cs +++ b/BTCPayServer/Controllers/AccessTokenController.cs @@ -21,7 +21,7 @@ namespace BTCPayServer.Controllers } [HttpGet] [Route("tokens")] - public async Task GetTokens() + public async Task Tokens() { var tokens = await _TokenRepository.GetTokens(this.GetBitIdentity().SIN); return new GetTokensResponse(tokens); @@ -29,33 +29,51 @@ namespace BTCPayServer.Controllers [HttpPost] [Route("tokens")] - public async Task>> GetPairingCode([FromBody] PairingCodeRequest token) + public async Task>> Tokens([FromBody] TokenRequest request) { - var now = DateTimeOffset.UtcNow; - var pairingEntity = new PairingCodeEntity() + PairingCodeEntity pairingEntity = null; + if(string.IsNullOrEmpty(request.PairingCode)) { - Facade = token.Facade, - Label = token.Label, - SIN = token.Id, - PairingTime = now, - PairingExpiration = now + TimeSpan.FromMinutes(15) - }; - var grantedToken = await _TokenRepository.CreateToken(token.Id, token.Facade); - pairingEntity.Token = grantedToken.Name; - pairingEntity = await _TokenRepository.AddPairingCodeAsync(pairingEntity); + if(string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id)) + throw new BitpayHttpException(400, "'id' property is required"); + if(string.IsNullOrEmpty(request.Facade)) + throw new BitpayHttpException(400, "'facade' property is required"); + + var pairingCode = await _TokenRepository.CreatePairingCodeAsync(); + await _TokenRepository.PairWithSINAsync(pairingCode, request.Id); + pairingEntity = await _TokenRepository.UpdatePairingCode(new PairingCodeEntity() + { + Id = pairingCode, + Facade = request.Facade, + Label = request.Label + }); + + } + else + { + var sin = this.GetBitIdentity(false)?.SIN ?? request.Id; + if(string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id)) + throw new BitpayHttpException(400, "'id' property is required, alternatively, use BitId"); + + pairingEntity = await _TokenRepository.GetPairingAsync(request.PairingCode); + pairingEntity.SIN = sin; + if(!await _TokenRepository.PairWithSINAsync(request.PairingCode, sin)) + throw new BitpayHttpException(400, "Unknown pairing code"); + + } var pairingCodes = new List - { - new PairingCodeResponse() { - PairingCode = pairingEntity.Id, - PairingExpiration = pairingEntity.PairingExpiration, - DateCreated = pairingEntity.PairingTime, - Facade = grantedToken.Name, - Token = grantedToken.Value, - Label = pairingEntity.Label - } - }; + new PairingCodeResponse() + { + PairingCode = pairingEntity.Id, + PairingExpiration = pairingEntity.Expiration, + DateCreated = pairingEntity.CreatedTime, + Facade = pairingEntity.Facade, + Token = pairingEntity.TokenValue, + Label = pairingEntity.Label + } + }; return DataWrapper.Create(pairingCodes); } } diff --git a/BTCPayServer/Controllers/InvoiceController.API.cs b/BTCPayServer/Controllers/InvoiceController.API.cs index 61bb7ee74..4ba230a1d 100644 --- a/BTCPayServer/Controllers/InvoiceController.API.cs +++ b/BTCPayServer/Controllers/InvoiceController.API.cs @@ -83,26 +83,26 @@ namespace BTCPayServer.Controllers if(facade == null) throw new ArgumentNullException(nameof(facade)); - var actualTokens = (await _TokenRepository.GetTokens(this.GetBitIdentity().SIN)).Where(t => t.Active).ToArray(); + var actualTokens = (await _TokenRepository.GetTokens(this.GetBitIdentity().SIN)).ToArray(); actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray(); var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal)); if(expectedToken == null || actualToken == null) { Logs.PayServer.LogDebug($"No token found for facade {facade} for SIN {this.GetBitIdentity().SIN}"); - throw new BitpayHttpException(401, $"This endpoint does not support the `{actualTokens.Select(a => a.Name).Concat(new[] { "user" }).FirstOrDefault()}` facade"); + throw new BitpayHttpException(401, $"This endpoint does not support the `{actualTokens.Select(a => a.Facade).Concat(new[] { "user" }).FirstOrDefault()}` facade"); } return actualToken; } private IEnumerable GetCompatibleTokens(BitTokenEntity token) { - if(token.Name == Facade.Merchant.ToString()) + if(token.Facade == Facade.Merchant.ToString()) { yield return token.Clone(Facade.User); yield return token.Clone(Facade.PointOfSale); } - if(token.Name == Facade.PointOfSale.ToString()) + if(token.Facade == Facade.PointOfSale.ToString()) { yield return token.Clone(Facade.User); } @@ -111,7 +111,7 @@ namespace BTCPayServer.Controllers private async Task FindStore(BitTokenEntity bitToken) { - var store = await _StoreRepository.FindStore(bitToken.PairedId); + var store = await _StoreRepository.FindStore(bitToken.StoreId); if(store == null) throw new BitpayHttpException(401, "Unknown store"); return store; diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index a69de783c..693174264 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -33,6 +33,8 @@ using BTCPayServer.Servcices.Invoices; using BTCPayServer.Services.Rates; using BTCPayServer.Services.Wallets; using BTCPayServer.Validations; + +using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Mvc.Routing; namespace BTCPayServer.Controllers diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 8a8b8e0f6..fc8f71571 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -197,11 +197,11 @@ namespace BTCPayServer.Controllers public async Task ListTokens(string storeId) { var model = new TokensViewModel(); - var tokens = await _TokenRepository.GetTokensByPairedIdAsync(storeId); + var tokens = await _TokenRepository.GetTokensByStoreIdAsync(storeId); model.StatusMessage = StatusMessage; model.Tokens = tokens.Select(t => new TokenViewModel() { - Facade = t.Name, + Facade = t.Facade, Label = t.Label, SIN = t.SIN, Id = t.Value @@ -219,16 +219,34 @@ namespace BTCPayServer.Controllers return View(model); } - var pairingCode = await _TokenController.GetPairingCode(new PairingCodeRequest() + var tokenRequest = new TokenRequest() { Facade = model.Facade, Label = model.Label, - Id = NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(new PubKey(model.PublicKey)) - }); + Id = model.PublicKey == null ? null : NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(new PubKey(model.PublicKey)) + }; + + string pairingCode = null; + if(model.PublicKey == null) + { + tokenRequest.PairingCode = await _TokenRepository.CreatePairingCodeAsync(); + await _TokenRepository.UpdatePairingCode(new PairingCodeEntity() + { + Id = tokenRequest.PairingCode, + Facade = model.Facade, + Label = model.Label, + }); + await _TokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, storeId); + pairingCode = tokenRequest.PairingCode; + } + else + { + pairingCode = ((DataWrapper>)await _TokenController.Tokens(tokenRequest)).Data[0].PairingCode; + } return RedirectToAction(nameof(RequestPairing), new { - pairingCode = pairingCode.Data[0].PairingCode, + pairingCode = pairingCode, selectedStore = storeId }); } @@ -239,22 +257,21 @@ namespace BTCPayServer.Controllers { var model = new CreateTokenViewModel(); model.Facade = "merchant"; - if(_Env.IsDevelopment()) - { - model.PublicKey = new Key().PubKey.ToHex(); - } return View(model); } [HttpPost] [ValidateAntiForgeryToken] [Route("{storeId}/Tokens/Delete")] - public async Task DeleteToken(string storeId, string name, string sin) + public async Task DeleteToken(string storeId, string tokenId) { - if(await _TokenRepository.DeleteToken(sin, name, storeId)) - StatusMessage = "Token revoked"; - else + var token = await _TokenRepository.GetToken(tokenId); + if(token == null || + token.StoreId != storeId || + !await _TokenRepository.DeleteToken(tokenId)) StatusMessage = "Failure to revoke this token"; + else + StatusMessage = "Token revoked"; return RedirectToAction(nameof(ListTokens)); } @@ -277,7 +294,7 @@ namespace BTCPayServer.Controllers Id = pairing.Id, Facade = pairing.Facade, Label = pairing.Label, - SIN = pairing.SIN, + SIN = pairing.SIN ?? "Server-Initiated Pairing", SelectedStore = selectedStore ?? stores.FirstOrDefault()?.Id, Stores = stores.Select(s => new PairingModel.StoreViewModel() { @@ -294,11 +311,14 @@ namespace BTCPayServer.Controllers public async Task Pair(string pairingCode, string selectedStore) { var store = await _Repo.FindStore(selectedStore, GetUserId()); - if(store == null) + var pairing = await _TokenRepository.GetPairingAsync(pairingCode); + if(store == null || pairing == null) return NotFound(); - if(pairingCode != null && await _TokenRepository.PairWithAsync(pairingCode, store.Id)) + if(pairingCode != null && await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id)) { StatusMessage = "Pairing is successfull"; + if(pairing.SIN == null) + StatusMessage = "Server initiated pairing code: " + pairingCode; return RedirectToAction(nameof(ListTokens), new { storeId = store.Id diff --git a/BTCPayServer/Data/ApplicationDbContext.cs b/BTCPayServer/Data/ApplicationDbContext.cs index b210f72b8..f3a494552 100644 --- a/BTCPayServer/Data/ApplicationDbContext.cs +++ b/BTCPayServer/Data/ApplicationDbContext.cs @@ -56,6 +56,17 @@ namespace BTCPayServer.Data get; set; } + + public DbSet PairingCodes + { + get; set; + } + + public DbSet PairedSINData + { + get; set; + } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { var isConfigured = optionsBuilder.Options.Extensions.OfType().Any(); @@ -94,6 +105,15 @@ namespace BTCPayServer.Data builder.Entity() .HasKey(o => o.Address); + + builder.Entity() + .HasKey(o => o.Id); + + builder.Entity(b => + { + b.HasIndex(o => o.SIN); + b.HasIndex(o => o.StoreDataId); + }); } } } diff --git a/BTCPayServer/Data/PairedSINData.cs b/BTCPayServer/Data/PairedSINData.cs new file mode 100644 index 000000000..8355fa5fa --- /dev/null +++ b/BTCPayServer/Data/PairedSINData.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Data +{ + public class PairedSINData + { + public string Id + { + get; set; + } + + public string Facade + { + get; set; + } + + public string StoreDataId + { + get; set; + } + public string Label + { + get; + set; + } + public DateTimeOffset PairingTime + { + get; + set; + } + public string SIN + { + get; set; + } + } +} diff --git a/BTCPayServer/Data/PairingCodeData.cs b/BTCPayServer/Data/PairingCodeData.cs new file mode 100644 index 000000000..44f2ecf56 --- /dev/null +++ b/BTCPayServer/Data/PairingCodeData.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Data +{ + public class PairingCodeData + { + public string Id + { + get; set; + } + + public string Facade + { + get; set; + } + public string StoreDataId + { + get; set; + } + public DateTimeOffset Expiration + { + get; + set; + } + + public string Label + { + get; + set; + } + public string SIN + { + get; + set; + } + public DateTime DateCreated + { + get; + set; + } + public string TokenValue + { + get; + set; + } + } +} diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 9501cbd88..40fc8c948 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -38,10 +38,10 @@ namespace BTCPayServer } - public static BitIdentity GetBitIdentity(this Controller controller) + public static BitIdentity GetBitIdentity(this Controller controller, bool throws = true) { if(!(controller.User.Identity is BitIdentity)) - throw new UnauthorizedAccessException("no-bitid"); + return throws ? throw new UnauthorizedAccessException("no-bitid") : (BitIdentity)null; return (BitIdentity)controller.User.Identity; } } diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 3a2331725..801157206 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -32,6 +32,7 @@ using BTCPayServer.Models; using System.Threading.Tasks; using System.Threading; using BTCPayServer.Services.Wallets; +using BTCPayServer.Authentication; namespace BTCPayServer.Hosting { @@ -107,7 +108,7 @@ namespace BTCPayServer.Hosting runtime.Configure(o.GetRequiredService()); return runtime; }); - services.TryAddSingleton(o => o.GetRequiredService().TokenRepository); + services.TryAddSingleton(); services.TryAddSingleton(o => o.GetRequiredService().InvoiceRepository); services.TryAddSingleton(o => o.GetRequiredService().Network); services.TryAddSingleton(o => o.GetRequiredService().DBFactory); diff --git a/BTCPayServer/Migrations/20171010082424_Tokens.Designer.cs b/BTCPayServer/Migrations/20171010082424_Tokens.Designer.cs new file mode 100644 index 000000000..af6f94745 --- /dev/null +++ b/BTCPayServer/Migrations/20171010082424_Tokens.Designer.cs @@ -0,0 +1,444 @@ +// +using BTCPayServer.Data; +using BTCPayServer.Servcices.Invoices; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using System; + +namespace BTCPayServer.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20171010082424_Tokens")] + partial class Tokens + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.Property("Address") + .ValueGeneratedOnAdd(); + + b.Property("InvoiceDataId"); + + b.HasKey("Address"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("AddressInvoices"); + }); + + 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.PairedSINData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Facade"); + + b.Property("Label"); + + b.Property("Name"); + + 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("Name"); + + 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("Blob"); + + b.Property("InvoiceDataId"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("Payments"); + }); + + 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("DerivationStrategy"); + + b.Property("SpeedPolicy"); + + 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("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() + .HasForeignKey("InvoiceDataId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany() + .HasForeignKey("StoreDataId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("Payments") + .HasForeignKey("InvoiceDataId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("RefundAddresses") + .HasForeignKey("InvoiceDataId"); + }); + + 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("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/20171010082424_Tokens.cs b/BTCPayServer/Migrations/20171010082424_Tokens.cs new file mode 100644 index 000000000..dc2669da3 --- /dev/null +++ b/BTCPayServer/Migrations/20171010082424_Tokens.cs @@ -0,0 +1,67 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace BTCPayServer.Migrations +{ + public partial class Tokens : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PairedSINData", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Facade = table.Column(type: "TEXT", nullable: true), + Label = table.Column(type: "TEXT", nullable: true), + Name = table.Column(type: "TEXT", nullable: true), + PairingTime = table.Column(nullable: false), + SIN = table.Column(type: "TEXT", nullable: true), + StoreDataId = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PairedSINData", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "PairingCodes", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + DateCreated = table.Column(nullable: false), + Expiration = table.Column(nullable: false), + Facade = table.Column(type: "TEXT", nullable: true), + Label = table.Column(type: "TEXT", nullable: true), + Name = table.Column(type: "TEXT", nullable: true), + SIN = table.Column(type: "TEXT", nullable: true), + StoreDataId = table.Column(type: "TEXT", nullable: true), + TokenValue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PairingCodes", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_PairedSINData_SIN", + table: "PairedSINData", + column: "SIN"); + + migrationBuilder.CreateIndex( + name: "IX_PairedSINData_StoreDataId", + table: "PairedSINData", + column: "StoreDataId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PairedSINData"); + + migrationBuilder.DropTable( + name: "PairingCodes"); + } + } +} diff --git a/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs index 2b972fd63..c0283b9a5 100644 --- a/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs @@ -62,6 +62,58 @@ namespace BTCPayServer.Migrations b.ToTable("Invoices"); }); + modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Facade"); + + b.Property("Label"); + + b.Property("Name"); + + 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("Name"); + + b.Property("SIN"); + + b.Property("StoreDataId"); + + b.Property("TokenValue"); + + b.HasKey("Id"); + + b.ToTable("PairingCodes"); + }); + modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => { b.Property("Id") diff --git a/BTCPayServer/Models/GetTokensResponse.cs b/BTCPayServer/Models/GetTokensResponse.cs index 716b03b5e..1a29a72d3 100644 --- a/BTCPayServer/Models/GetTokensResponse.cs +++ b/BTCPayServer/Models/GetTokensResponse.cs @@ -39,7 +39,7 @@ namespace BTCPayServer.Models { JObject item = new JObject(); jarray.Add(item); - JProperty jProp = new JProperty(token.Name); + JProperty jProp = new JProperty(token.Facade); item.Add(jProp); jProp.Value = token.Value; } diff --git a/BTCPayServer/Models/TokenRequest.cs b/BTCPayServer/Models/TokenRequest.cs index 97e26269e..8513a01f5 100644 --- a/BTCPayServer/Models/TokenRequest.cs +++ b/BTCPayServer/Models/TokenRequest.cs @@ -6,7 +6,7 @@ using NBitcoin; namespace BTCPayServer.Models { - public class PairingCodeRequest + public class TokenRequest { [JsonProperty(PropertyName = "id")] public string Id @@ -34,6 +34,12 @@ namespace BTCPayServer.Models { get; set; } + + [JsonProperty(PropertyName = "pairingCode")] + public string PairingCode + { + get; set; + } } public class PairingCodeResponse diff --git a/BTCPayServer/Views/Stores/CreateToken.cshtml b/BTCPayServer/Views/Stores/CreateToken.cshtml index aad53a295..e46fba786 100644 --- a/BTCPayServer/Views/Stores/CreateToken.cshtml +++ b/BTCPayServer/Views/Stores/CreateToken.cshtml @@ -17,6 +17,7 @@
+ Keep empty for server-initiated pairing
diff --git a/BTCPayServer/Views/Stores/ListTokens.cshtml b/BTCPayServer/Views/Stores/ListTokens.cshtml index a230fb313..77222cbbd 100644 --- a/BTCPayServer/Views/Stores/ListTokens.cshtml +++ b/BTCPayServer/Views/Stores/ListTokens.cshtml @@ -27,8 +27,7 @@ @token.Facade
- - +