From 2848caff2ee9b4d87c8132efcb63cb431a7eac36 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sun, 29 Apr 2018 18:28:04 +0900 Subject: [PATCH] Support Legacy API Key authentication to Bitpay Invoice API --- BTCPayServer.Tests/UnitTest1.cs | 33 ++ BTCPayServer.Tests/UnitTestPeusa.cs | 52 -- .../Authentication/TokenRepository.cs | 40 ++ BTCPayServer/Controllers/StoresController.cs | 17 + BTCPayServer/Data/APIKeyData.cs | 23 + BTCPayServer/Data/ApplicationDbContext.cs | 7 + BTCPayServer/Data/StoreData.cs | 3 +- .../Filters/OnlyMediaTypeAttribute.cs | 6 +- BTCPayServer/Hosting/BTCpayMiddleware.cs | 43 +- .../20180429083930_legacyapikey.Designer.cs | 553 ++++++++++++++++++ .../Migrations/20180429083930_legacyapikey.cs | 35 ++ .../ApplicationDbContextModelSnapshot.cs | 18 +- .../Models/StoreViewModels/TokensViewModel.cs | 4 + BTCPayServer/Views/Stores/ListTokens.cshtml | 84 ++- 14 files changed, 831 insertions(+), 87 deletions(-) delete mode 100644 BTCPayServer.Tests/UnitTestPeusa.cs create mode 100644 BTCPayServer/Data/APIKeyData.cs create mode 100644 BTCPayServer/Migrations/20180429083930_legacyapikey.Designer.cs create mode 100644 BTCPayServer/Migrations/20180429083930_legacyapikey.cs diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 64afa1d28..1b7eccc60 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -32,6 +32,9 @@ using BTCPayServer.HostedServices; using BTCPayServer.Payments.Lightning; using BTCPayServer.Models.AppViewModels; using BTCPayServer.Services.Apps; +using BTCPayServer.Services.Stores; +using System.Net.Http; +using System.Text; namespace BTCPayServer.Tests { @@ -623,6 +626,36 @@ namespace BTCPayServer.Tests user.GrantAccess(); user.RegisterDerivationScheme("BTC"); Assert.True(user.BitPay.TestAccess(Facade.Merchant)); + + // Can generate API Key + var repo = tester.PayTester.GetService(); + Assert.Empty(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult()); + Assert.IsType(user.GetController().GenerateAPIKey(user.StoreId).GetAwaiter().GetResult()); + + var apiKey = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult()); + /////// + + // Generating a new one remove the previous + Assert.IsType(user.GetController().GenerateAPIKey(user.StoreId).GetAwaiter().GetResult()); + var apiKey2 = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult()); + Assert.NotEqual(apiKey, apiKey2); + //////// + + apiKey = apiKey2; + + // Can create an invoice with this new API Key + HttpClient client = new HttpClient(); + HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, tester.PayTester.ServerUri.AbsoluteUri + "invoices"); + message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(apiKey))); + var invoice = new Invoice() + { + Price = 5000.0, + Currency = "USD" + }; + message.Content = new StringContent(JsonConvert.SerializeObject(invoice), Encoding.UTF8, "application/json"); + var result = client.SendAsync(message).GetAwaiter().GetResult(); + result.EnsureSuccessStatusCode(); + ///////////////////// } } diff --git a/BTCPayServer.Tests/UnitTestPeusa.cs b/BTCPayServer.Tests/UnitTestPeusa.cs deleted file mode 100644 index cb9861d5f..000000000 --- a/BTCPayServer.Tests/UnitTestPeusa.cs +++ /dev/null @@ -1,52 +0,0 @@ -using NBitcoin; -using NBitcoin.DataEncoders; -using NBitpayClient; -using System; -using System.Collections.Generic; -using System.Text; -using Xunit; - -namespace BTCPayServer.Tests -{ - // Helper class for testing functionality and generating data needed during coding/debuging - public class UnitTestPeusa - { - // Unit test that generates temorary checkout Bitpay page - // https://forkbitpay.slack.com/archives/C7M093Z55/p1508293682000217 - - // Testnet of Bitpay down - //[Fact] - //public void BitpayCheckout() - //{ - // var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056")); - // var url = new Uri("https://test.bitpay.com/"); - // var btcpay = new Bitpay(key, url); - // var invoice = btcpay.CreateInvoice(new Invoice() - // { - - // Price = 5.0, - // Currency = "USD", - // PosData = "posData", - // OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73", - // ItemDesc = "Hello from the otherside" - // }, Facade.Merchant); - - // // go to invoice.Url - // Console.WriteLine(invoice.Url); - //} - - // Generating Extended public key to use on http://localhost:14142/stores/{storeId} - [Fact] - public void GeneratePubkey() - { - var network = Network.RegTest; - - ExtKey masterKey = new ExtKey(); - Console.WriteLine("Master key : " + masterKey.ToString(network)); - ExtPubKey masterPubKey = masterKey.Neuter(); - - ExtPubKey pubkey = masterPubKey.Derive(0); - Console.WriteLine("PubKey " + 0 + " : " + pubkey.ToString(network)); - } - } -} diff --git a/BTCPayServer/Authentication/TokenRepository.cs b/BTCPayServer/Authentication/TokenRepository.cs index 0473aadca..776533fe3 100644 --- a/BTCPayServer/Authentication/TokenRepository.cs +++ b/BTCPayServer/Authentication/TokenRepository.cs @@ -45,6 +45,46 @@ namespace BTCPayServer.Authentication } } + public async Task GetStoreIdFromAPIKey(string apiKey) + { + using (var ctx = _Factory.CreateContext()) + { + return await ctx.ApiKeys.Where(o => o.Id == apiKey).Select(o => o.StoreId).FirstOrDefaultAsync(); + } + } + + public async Task GenerateLegacyAPIKey(string storeId) + { + // It is legacy support and Bitpay generate string of unknown format, trying to replicate them + // as good as possible. The string below got generated for me. + var chars = "ERo0vkBMOYhyU0ZHvirCplbLDIGWPdi1ok77VnW7QdE"; + var rand = new Random(Math.Abs(RandomUtils.GetInt32())); + var generated = new char[chars.Length]; + for (int i = 0; i < generated.Length; i++) + { + generated[i] = chars[rand.Next(0, generated.Length)]; + } + + using (var ctx = _Factory.CreateContext()) + { + var existing = await ctx.ApiKeys.Where(o => o.StoreId == storeId).FirstOrDefaultAsync(); + if (existing != null) + { + ctx.ApiKeys.Remove(existing); + } + ctx.ApiKeys.Add(new APIKeyData() { Id = new string(generated), StoreId = storeId }); + await ctx.SaveChangesAsync().ConfigureAwait(false); + } + } + + public async Task GetLegacyAPIKeys(string storeId) + { + using (var ctx = _Factory.CreateContext()) + { + return await ctx.ApiKeys.Where(o => o.StoreId == storeId).Select(c => c.Id).ToArrayAsync(); + } + } + private BitTokenEntity CreateTokenEntity(PairedSINData data) { return new BitTokenEntity() diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 5b33e4c63..69b911d5c 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -427,6 +427,12 @@ namespace BTCPayServer.Controllers SIN = t.SIN, Id = t.Value }).ToArray(); + + model.ApiKey = (await _TokenRepository.GetLegacyAPIKeys(storeId)).FirstOrDefault(); + if (model.ApiKey == null) + model.EncodedApiKey = "*API Key*"; + else + model.EncodedApiKey = Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(model.ApiKey)); return View(model); } @@ -525,6 +531,17 @@ namespace BTCPayServer.Controllers return RedirectToAction(nameof(ListTokens)); } + [HttpPost] + [Route("{storeId}/tokens/apikey")] + public async Task GenerateAPIKey(string storeId) + { + var store = await _Repo.FindStore(storeId, GetUserId()); + if (store == null) + return NotFound(); + await _TokenRepository.GenerateLegacyAPIKey(storeId); + StatusMessage = "API Key re-generated"; + return RedirectToAction(nameof(ListTokens)); + } [HttpGet] [Route("/api-access-request")] diff --git a/BTCPayServer/Data/APIKeyData.cs b/BTCPayServer/Data/APIKeyData.cs new file mode 100644 index 000000000..e826c32f7 --- /dev/null +++ b/BTCPayServer/Data/APIKeyData.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Data +{ + public class APIKeyData + { + [MaxLength(50)] + public string Id + { + get; set; + } + + [MaxLength(50)] + public string StoreId + { + get; set; + } + } +} diff --git a/BTCPayServer/Data/ApplicationDbContext.cs b/BTCPayServer/Data/ApplicationDbContext.cs index 9ac63c03b..fb4507a99 100644 --- a/BTCPayServer/Data/ApplicationDbContext.cs +++ b/BTCPayServer/Data/ApplicationDbContext.cs @@ -86,6 +86,11 @@ namespace BTCPayServer.Data get; set; } + public DbSet ApiKeys + { + get; set; + } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { var isConfigured = optionsBuilder.Options.Extensions.OfType().Any(); @@ -112,6 +117,8 @@ namespace BTCPayServer.Data t.StoreDataId }); + builder.Entity() + .HasIndex(o => o.StoreId); builder.Entity() .HasOne(a => a.StoreData); diff --git a/BTCPayServer/Data/StoreData.cs b/BTCPayServer/Data/StoreData.cs index 58923bbf1..52c19e26a 100644 --- a/BTCPayServer/Data/StoreData.cs +++ b/BTCPayServer/Data/StoreData.cs @@ -14,6 +14,7 @@ using Newtonsoft.Json.Linq; using BTCPayServer.Services.Rates; using BTCPayServer.Payments; using BTCPayServer.JsonConverters; +using System.ComponentModel.DataAnnotations; namespace BTCPayServer.Data { @@ -120,7 +121,7 @@ namespace BTCPayServer.Data } } - if(!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain) + if (!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain) { DerivationStrategy = null; } diff --git a/BTCPayServer/Filters/OnlyMediaTypeAttribute.cs b/BTCPayServer/Filters/OnlyMediaTypeAttribute.cs index abca0f908..61110d697 100644 --- a/BTCPayServer/Filters/OnlyMediaTypeAttribute.cs +++ b/BTCPayServer/Filters/OnlyMediaTypeAttribute.cs @@ -44,8 +44,10 @@ namespace BTCPayServer.Filters public bool Accept(ActionConstraintContext context) { var hasVersion = context.RouteContext.HttpContext.Request.Headers["x-accept-version"].Where(h => h == "2.0.0").Any(); - var hasIdentity = context.RouteContext.HttpContext.Request.Headers["x-identity"].Any(); - return (hasVersion || hasIdentity) == IsBitpayAPI; + var isBitpayAPI = + context.RouteContext.HttpContext.Items.TryGetValue("IsBitpayAPI", out object obj) && + obj is bool b && b; + return (hasVersion || isBitpayAPI) == IsBitpayAPI; } } diff --git a/BTCPayServer/Hosting/BTCpayMiddleware.cs b/BTCPayServer/Hosting/BTCpayMiddleware.cs index 5eab8d6c5..69b4951db 100644 --- a/BTCPayServer/Hosting/BTCpayMiddleware.cs +++ b/BTCPayServer/Hosting/BTCpayMiddleware.cs @@ -53,12 +53,23 @@ namespace BTCPayServer.Hosting var sig = values.FirstOrDefault(); httpContext.Request.Headers.TryGetValue("x-identity", out values); var id = values.FirstOrDefault(); + httpContext.Request.Headers.TryGetValue("Authorization", out values); + var auth = values.FirstOrDefault(); try { + bool isBitId = false; if (!string.IsNullOrEmpty(sig) && !string.IsNullOrEmpty(id)) { await HandleBitId(httpContext, sig, id); + isBitId = httpContext.User.HasClaim(c => c.Type == Claims.SIN); + if (!isBitId) + Logs.PayServer.LogDebug("BitId signature check failed"); } + if (!isBitId && !string.IsNullOrEmpty(auth)) + { + await HandleLegacyAPIKey(httpContext, auth); + } + await _Next(httpContext); } catch (WebSocketException) @@ -76,7 +87,7 @@ namespace BTCPayServer.Hosting Logs.PayServer.LogCritical(new EventId(), ex, "Unhandled exception in BTCPayMiddleware"); throw; } - } + } private void RewriteHostIfNeeded(HttpContext httpContext) { @@ -221,11 +232,37 @@ namespace BTCPayServer.Hosting identity.AddClaim(new Claim(Claims.OwnStore, bitToken.StoreId)); } Logs.PayServer.LogDebug($"BitId signature check success for SIN {sin}"); + NBitcoin.Extensions.TryAdd(httpContext.Items, "IsBitpayAPI", true); } } catch (FormatException) { } - if (!httpContext.User.HasClaim(c => c.Type == Claims.SIN)) - Logs.PayServer.LogDebug("BitId signature check failed"); + } + + private async Task HandleLegacyAPIKey(HttpContext httpContext, string auth) + { + var splitted = auth.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase)) + { + throw new BitpayHttpException(401, $"Invalid Authorization header"); + } + + string apiKey = null; + try + { + apiKey = Encoders.ASCII.EncodeData(Encoders.Base64.DecodeData(splitted[1])); + } + catch + { + throw new BitpayHttpException(401, $"Invalid Authorization header"); + } + var storeId = await _TokenRepository.GetStoreIdFromAPIKey(apiKey); + if (storeId == null) + { + throw new BitpayHttpException(401, $"Invalid Authorization header"); + } + var identity = ((ClaimsIdentity)httpContext.User.Identity); + identity.AddClaim(new Claim(Claims.OwnStore, storeId)); + NBitcoin.Extensions.TryAdd(httpContext.Items, "IsBitpayAPI", true); } private async Task GetTokenPermissionAsync(string sin, string expectedToken) diff --git a/BTCPayServer/Migrations/20180429083930_legacyapikey.Designer.cs b/BTCPayServer/Migrations/20180429083930_legacyapikey.Designer.cs new file mode 100644 index 000000000..b478184d6 --- /dev/null +++ b/BTCPayServer/Migrations/20180429083930_legacyapikey.Designer.cs @@ -0,0 +1,553 @@ +// +using BTCPayServer.Data; +using BTCPayServer.Services.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("20180429083930_legacyapikey")] + partial class legacyapikey + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.2-rtm-10011"); + + 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.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("Facade"); + + 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") + .ValueGeneratedOnAdd(); + + 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("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"); + }); + + modelBuilder.Entity("BTCPayServer.Data.AppData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("Apps") + .HasForeignKey("StoreDataId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData") + .WithMany("HistoricalAddressInvoices") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany() + .HasForeignKey("StoreDataId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData") + .WithMany("Events") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + 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/20180429083930_legacyapikey.cs b/BTCPayServer/Migrations/20180429083930_legacyapikey.cs new file mode 100644 index 000000000..0a1d21bb5 --- /dev/null +++ b/BTCPayServer/Migrations/20180429083930_legacyapikey.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace BTCPayServer.Migrations +{ + public partial class legacyapikey : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ApiKeys", + columns: table => new + { + Id = table.Column(maxLength: 50, nullable: false), + StoreId = table.Column(maxLength: 50, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiKeys", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeys_StoreId", + table: "ApiKeys", + column: "StoreId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ApiKeys"); + } + } +} diff --git a/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs index 196e557f9..c31f3ff53 100644 --- a/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ namespace BTCPayServer.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "2.0.1-rtm-125"); + .HasAnnotation("ProductVersion", "2.0.2-rtm-10011"); modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => { @@ -36,6 +36,22 @@ namespace BTCPayServer.Migrations 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") diff --git a/BTCPayServer/Models/StoreViewModels/TokensViewModel.cs b/BTCPayServer/Models/StoreViewModels/TokensViewModel.cs index 694b5d64d..84e9791b5 100644 --- a/BTCPayServer/Models/StoreViewModels/TokensViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/TokensViewModel.cs @@ -68,5 +68,9 @@ namespace BTCPayServer.Models.StoreViewModels get; set; } + + [Display(Name = "API Key")] + public string ApiKey { get; set; } + public string EncodedApiKey { get; set; } } } diff --git a/BTCPayServer/Views/Stores/ListTokens.cshtml b/BTCPayServer/Views/Stores/ListTokens.cshtml index eccc1a77b..7aefec98d 100644 --- a/BTCPayServer/Views/Stores/ListTokens.cshtml +++ b/BTCPayServer/Views/Stores/ListTokens.cshtml @@ -5,32 +5,60 @@ ViewData.AddActivePage(StoreNavPages.Tokens); } -

@ViewData["Title"]

-

You can allow a public key to access the API of this store

@Html.Partial("_StatusMessage", Model.StatusMessage) - Create a new token - - - - - - - - - - - @foreach (var token in Model.Tokens) - { - - - - - - } - -
LabelSINFacadeActions
@token.Label@token.SIN@token.Facade -
- - -
-
+

Access token

+
+
+

Authorize a public key to access Bitpay compatible Invoice API (More information)

+
+
+
+
+ Create a new token + + + + + + + + + + + @foreach(var token in Model.Tokens) + { + + + + + + + } + +
LabelSINFacadeActions
@token.Label@token.SIN@token.Facade +
+ + +
+
+
+
+ +

Legacy API Keys

+
+
+

Alternatively, you can use the invoice API by including the following HTTP Header in your requests:
Authorization: Basic @Model.EncodedApiKey

+
+
+ +
+
+
+
+ + +
+ +
+
+