Merge remote-tracking branch 'upstream/master'

This commit is contained in:
cronos 2018-04-30 10:29:30 -05:00
commit 25208915eb
65 changed files with 2007 additions and 628 deletions

View File

@ -2,8 +2,10 @@
using BTCPayServer.Hosting; using BTCPayServer.Hosting;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Tests.Logging; using BTCPayServer.Tests.Logging;
using BTCPayServer.Tests.Mocks; using BTCPayServer.Tests.Mocks;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
@ -142,7 +144,7 @@ namespace BTCPayServer.Tests
return _Host.Services.GetRequiredService<T>(); return _Host.Services.GetRequiredService<T>();
} }
public T GetController<T>(string userId = null) where T : Controller public T GetController<T>(string userId = null, string storeId = null) where T : Controller
{ {
var context = new DefaultHttpContext(); var context = new DefaultHttpContext();
context.Request.Host = new HostString("127.0.0.1"); context.Request.Host = new HostString("127.0.0.1");
@ -150,7 +152,11 @@ namespace BTCPayServer.Tests
context.Request.Protocol = "http"; context.Request.Protocol = "http";
if (userId != null) if (userId != null)
{ {
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) })); context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, Policies.CookieAuthentication));
}
if(storeId != null)
{
context.SetStoreData(GetService<StoreRepository>().FindStore(storeId, userId).GetAwaiter().GetResult());
} }
var scope = (IServiceScopeFactory)_Host.Services.GetService(typeof(IServiceScopeFactory)); var scope = (IServiceScopeFactory)_Host.Services.GetService(typeof(IServiceScopeFactory));
var provider = scope.CreateScope().ServiceProvider; var provider = scope.CreateScope().ServiceProvider;

View File

@ -44,29 +44,27 @@ namespace BTCPayServer.Tests
public async Task GrantAccessAsync() public async Task GrantAccessAsync()
{ {
await RegisterAsync(); await RegisterAsync();
var store = await CreateStoreAsync(); await CreateStoreAsync();
var store = this.GetController<StoresController>();
var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant); var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant);
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString())); Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
await store.Pair(pairingCode.ToString(), StoreId); await store.Pair(pairingCode.ToString(), StoreId);
} }
public StoresController CreateStore() public void CreateStore()
{ {
return CreateStoreAsync().GetAwaiter().GetResult(); CreateStoreAsync().GetAwaiter().GetResult();
} }
public T GetController<T>() where T : Controller public T GetController<T>(bool setImplicitStore = true) where T : Controller
{ {
return parent.PayTester.GetController<T>(UserId); return parent.PayTester.GetController<T>(UserId, setImplicitStore ? StoreId : null);
} }
public async Task<StoresController> CreateStoreAsync() public async Task CreateStoreAsync()
{ {
var store = parent.PayTester.GetController<UserStoresController>(UserId); var store = this.GetController<UserStoresController>();
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" }); await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
StoreId = store.CreatedStoreId; StoreId = store.CreatedStoreId;
var store2 = parent.PayTester.GetController<StoresController>(UserId);
store2.CreatedStoreId = store.CreatedStoreId;
return store2;
} }
public BTCPayNetwork SupportedNetwork { get; set; } public BTCPayNetwork SupportedNetwork { get; set; }
@ -78,12 +76,12 @@ namespace BTCPayServer.Tests
public async Task RegisterDerivationSchemeAsync(string cryptoCode) public async Task RegisterDerivationSchemeAsync(string cryptoCode)
{ {
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode); SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
var store = parent.PayTester.GetController<StoresController>(UserId); var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork); ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]"); DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
var vm = (StoreViewModel)((ViewResult)await store.UpdateStore(StoreId)).Model; var vm = (StoreViewModel)((ViewResult)store.UpdateStore(StoreId)).Model;
vm.SpeedPolicy = SpeedPolicy.MediumSpeed; vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
await store.UpdateStore(StoreId, vm); await store.UpdateStore(vm);
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel() await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
{ {
@ -127,7 +125,7 @@ namespace BTCPayServer.Tests
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType) public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
{ {
var storeController = parent.PayTester.GetController<StoresController>(UserId); var storeController = this.GetController<StoresController>();
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel() await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
{ {
Url = connectionType == LightningConnectionType.Charge ? parent.MerchantCharge.Client.Uri.AbsoluteUri : Url = connectionType == LightningConnectionType.Charge ? parent.MerchantCharge.Client.Uri.AbsoluteUri :

View File

@ -32,6 +32,9 @@ using BTCPayServer.HostedServices;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Models.AppViewModels; using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps; using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Stores;
using System.Net.Http;
using System.Text;
namespace BTCPayServer.Tests namespace BTCPayServer.Tests
{ {
@ -303,9 +306,9 @@ namespace BTCPayServer.Tests
tester.Start(); tester.Start();
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); user.GrantAccess();
var storeController = tester.PayTester.GetController<StoresController>(user.UserId); var storeController = user.GetController<StoresController>();
Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult()); Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId));
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId, "BTC").GetAwaiter().GetResult()); Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId, "BTC"));
var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel() var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
{ {
@ -319,7 +322,7 @@ namespace BTCPayServer.Tests
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
}, "save", "BTC").GetAwaiter().GetResult()); }, "save", "BTC").GetAwaiter().GetResult());
var storeVm = Assert.IsType<Models.StoreViewModels.StoreViewModel>(Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult()).Model); var storeVm = Assert.IsType<Models.StoreViewModels.StoreViewModel>(Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId)).Model);
Assert.Single(storeVm.LightningNodes.Where(l => !string.IsNullOrEmpty(l.Address))); Assert.Single(storeVm.LightningNodes.Where(l => !string.IsNullOrEmpty(l.Address)));
} }
} }
@ -465,8 +468,8 @@ namespace BTCPayServer.Tests
acc.Register(); acc.Register();
acc.CreateStore(); acc.CreateStore();
var controller = tester.PayTester.GetController<StoresController>(acc.UserId); var controller = acc.GetController<StoresController>();
var token = (RedirectToActionResult)controller.CreateToken(acc.StoreId, new Models.StoreViewModels.CreateTokenViewModel() var token = (RedirectToActionResult)controller.CreateToken(new Models.StoreViewModels.CreateTokenViewModel()
{ {
Facade = Facade.Merchant.ToString(), Facade = Facade.Merchant.ToString(),
Label = "bla", Label = "bla",
@ -524,13 +527,15 @@ namespace BTCPayServer.Tests
tester.Start(); tester.Start();
var acc = tester.NewAccount(); var acc = tester.NewAccount();
acc.Register(); acc.Register();
var store = acc.CreateStore(); acc.CreateStore();
var store = acc.GetController<StoresController>();
var pairingCode = acc.BitPay.RequestClientAuthorization("test", Facade.Merchant); var pairingCode = acc.BitPay.RequestClientAuthorization("test", Facade.Merchant);
Assert.IsType<RedirectToActionResult>(store.Pair(pairingCode.ToString(), acc.StoreId).GetAwaiter().GetResult()); Assert.IsType<RedirectToActionResult>(store.Pair(pairingCode.ToString(), acc.StoreId).GetAwaiter().GetResult());
pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant); pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant);
var store2 = acc.CreateStore(); acc.CreateStore();
store2.Pair(pairingCode.ToString(), store2.CreatedStoreId).GetAwaiter().GetResult(); var store2 = acc.GetController<StoresController>();
store2.Pair(pairingCode.ToString(), store2.StoreData.Id).GetAwaiter().GetResult();
Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage, StringComparison.CurrentCultureIgnoreCase); Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage, StringComparison.CurrentCultureIgnoreCase);
} }
} }
@ -598,8 +603,18 @@ namespace BTCPayServer.Tests
var search = new SearchString(filter); var search = new SearchString(filter);
Assert.Equal("storeid:abc status:abed blabhbalh", search.ToString()); Assert.Equal("storeid:abc status:abed blabhbalh", search.ToString());
Assert.Equal("blabhbalh", search.TextSearch); Assert.Equal("blabhbalh", search.TextSearch);
Assert.Equal("abc", search.Filters["storeid"]); Assert.Single(search.Filters["storeid"]);
Assert.Equal("abed", search.Filters["status"]); Assert.Single(search.Filters["status"]);
Assert.Equal("abc", search.Filters["storeid"].First());
Assert.Equal("abed", search.Filters["status"].First());
filter = "status:abed status:abed2";
search = new SearchString(filter);
Assert.Equal("status:abed status:abed2", search.ToString());
Assert.Throws<KeyNotFoundException>(() => search.Filters["test"]);
Assert.Equal(2, search.Filters["status"].Count);
Assert.Equal("abed", search.Filters["status"].First());
Assert.Equal("abed2", search.Filters["status"].Skip(1).First());
} }
[Fact] [Fact]
@ -613,6 +628,36 @@ namespace BTCPayServer.Tests
user.GrantAccess(); user.GrantAccess();
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
Assert.True(user.BitPay.TestAccess(Facade.Merchant)); Assert.True(user.BitPay.TestAccess(Facade.Merchant));
// Can generate API Key
var repo = tester.PayTester.GetService<TokenRepository>();
Assert.Empty(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().GenerateAPIKey().GetAwaiter().GetResult());
var apiKey = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
///////
// Generating a new one remove the previous
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().GenerateAPIKey().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();
/////////////////////
} }
} }
@ -633,7 +678,7 @@ namespace BTCPayServer.Tests
Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache
rates.Add(bitflyer); rates.Add(bitflyer);
foreach(var rate in rates) foreach (var rate in rates)
{ {
Assert.Single(rates.Where(r => r == rate)); Assert.Single(rates.Where(r => r == rate));
} }
@ -642,10 +687,10 @@ namespace BTCPayServer.Tests
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange) private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange)
{ {
var storeController = tester.PayTester.GetController<StoresController>(user.UserId); var storeController = user.GetController<StoresController>();
var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId).Result).Model; var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId)).Model;
vm.PreferredExchange = exchange; vm.PreferredExchange = exchange;
storeController.UpdateStore(user.StoreId, vm).Wait(); storeController.UpdateStore(vm).Wait();
var invoice2 = user.BitPay.CreateInvoice(new Invoice() var invoice2 = user.BitPay.CreateInvoice(new Invoice()
{ {
Price = 5000.0, Price = 5000.0,
@ -681,11 +726,11 @@ namespace BTCPayServer.Tests
}, Facade.Merchant); }, Facade.Merchant);
var storeController = tester.PayTester.GetController<StoresController>(user.UserId); var storeController = user.GetController<StoresController>();
var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId).Result).Model; var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId)).Model;
Assert.Equal(1.0, vm.RateMultiplier); Assert.Equal(1.0, vm.RateMultiplier);
vm.RateMultiplier = 0.5; vm.RateMultiplier = 0.5;
storeController.UpdateStore(user.StoreId, vm).Wait(); storeController.UpdateStore(vm).Wait();
var invoice2 = user.BitPay.CreateInvoice(new Invoice() var invoice2 = user.BitPay.CreateInvoice(new Invoice()
@ -920,10 +965,10 @@ namespace BTCPayServer.Tests
user.GrantAccess(); user.GrantAccess();
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
user.RegisterLightningNode("BTC", LightningConnectionType.Charge); user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience(user.StoreId).Result).Model); var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience()).Model);
vm.LightningMaxValue = "2 USD"; vm.LightningMaxValue = "2 USD";
vm.OnChainMinValue = "5 USD"; vm.OnChainMinValue = "5 USD";
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().CheckoutExperience(user.StoreId, vm).Result); Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().CheckoutExperience(vm).Result);
var invoice = user.BitPay.CreateInvoice(new Invoice() var invoice = user.BitPay.CreateInvoice(new Invoice()
{ {
@ -987,7 +1032,7 @@ namespace BTCPayServer.Tests
Assert.Equal("orange", vmview.Items[1].Title); Assert.Equal("orange", vmview.Items[1].Title);
Assert.Equal(10.0m, vmview.Items[1].Price.Value); Assert.Equal(10.0m, vmview.Items[1].Price.Value);
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted); Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
Assert.IsType<RedirectResult>(apps.ViewPointOfSale(appId, "orange").Result); Assert.IsType<RedirectResult>(apps.ViewPointOfSale(appId, 0, "orange").Result);
var invoice = user.BitPay.GetInvoices().First(); var invoice = user.BitPay.GetInvoices().First();
Assert.Equal(10.00, invoice.Price); Assert.Equal(10.00, invoice.Price);
Assert.Equal("CAD", invoice.Currency); Assert.Equal("CAD", invoice.Currency);
@ -1054,13 +1099,13 @@ namespace BTCPayServer.Tests
{ {
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery() var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{ {
StoreId = user.StoreId, StoreId = new[] { user.StoreId },
TextSearch = invoice.OrderId TextSearch = invoice.OrderId
}).GetAwaiter().GetResult(); }).GetAwaiter().GetResult();
Assert.Single(textSearchResult); Assert.Single(textSearchResult);
textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery() textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{ {
StoreId = user.StoreId, StoreId = new[] { user.StoreId },
TextSearch = invoice.Id TextSearch = invoice.Id
}).GetAwaiter().GetResult(); }).GetAwaiter().GetResult();
@ -1185,6 +1230,26 @@ namespace BTCPayServer.Tests
} }
} }
[Fact]
public void CheckQuadrigacxRateProvider()
{
var quadri = new QuadrigacxRateProvider("BTC");
var rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
Assert.NotEmpty(rates);
Assert.NotEqual(0.0m, rates.First().Value);
Assert.NotEqual(0.0m, quadri.GetRateAsync("CAD").GetAwaiter().GetResult());
Assert.NotEqual(0.0m, quadri.GetRateAsync("USD").GetAwaiter().GetResult());
Assert.Throws<RateUnavailableException>(() => quadri.GetRateAsync("IOEW").GetAwaiter().GetResult());
quadri = new QuadrigacxRateProvider("LTC");
rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
Assert.NotEmpty(rates);
Assert.NotEqual(0.0m, rates.First().Value);
Assert.NotEqual(0.0m, quadri.GetRateAsync("CAD").GetAwaiter().GetResult());
Assert.Throws<RateUnavailableException>(() => quadri.GetRateAsync("IOEW").GetAwaiter().GetResult());
Assert.Throws<RateUnavailableException>(() => quadri.GetRateAsync("USD").GetAwaiter().GetResult());
}
[Fact] [Fact]
public void CheckRatesProvider() public void CheckRatesProvider()
{ {

View File

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

View File

@ -46,7 +46,7 @@ services:
- lightning-charged - lightning-charged
nbxplorer: nbxplorer:
image: nicolasdorier/nbxplorer:1.0.2.0 image: nicolasdorier/nbxplorer:1.0.2.2
ports: ports:
- "32838:32838" - "32838:32838"
expose: expose:
@ -89,7 +89,7 @@ services:
- "bitcoin_datadir:/data" - "bitcoin_datadir:/data"
customer_lightningd: customer_lightningd:
image: nicolasdorier/clightning:0.0.0.3 image: nicolasdorier/clightning:0.0.0.11-dev
environment: environment:
EXPOSE_TCP: "true" EXPOSE_TCP: "true"
LIGHTNINGD_OPT: | LIGHTNINGD_OPT: |
@ -98,6 +98,7 @@ services:
network=regtest network=regtest
ipaddr=customer_lightningd ipaddr=customer_lightningd
log-level=debug log-level=debug
dev-broadcast-interval=1000
ports: ports:
- "30992:9835" # api port - "30992:9835" # api port
expose: expose:
@ -129,7 +130,7 @@ services:
- merchant_lightningd - merchant_lightningd
merchant_lightningd: merchant_lightningd:
image: nicolasdorier/clightning:0.0.0.5-dev image: nicolasdorier/clightning:0.0.0.11-dev
environment: environment:
EXPOSE_TCP: "true" EXPOSE_TCP: "true"
LIGHTNINGD_OPT: | LIGHTNINGD_OPT: |

View File

@ -1,35 +0,0 @@
using NBitcoin;
using NBitcoin.DataEncoders;
using System;
using System.Collections.Generic;
using System.Security.Principal;
using System.Text;
namespace BTCPayServer.Authentication
{
public class BitIdentity : IIdentity
{
public BitIdentity(PubKey key)
{
PubKey = key;
_Name = Encoders.Base58Check.EncodeData(Encoders.Hex.DecodeData("0f02" + key.Hash.ToString()));
SIN = NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(key);
}
string _Name;
public string SIN
{
get;
}
public PubKey PubKey
{
get;
}
public string AuthenticationType => "BitID";
public bool IsAuthenticated => true;
public string Name => _Name;
}
}

View File

@ -33,6 +33,8 @@ namespace BTCPayServer.Authentication
public async Task<BitTokenEntity[]> GetTokens(string sin) public async Task<BitTokenEntity[]> GetTokens(string sin)
{ {
if (sin == null)
return Array.Empty<BitTokenEntity>();
using (var ctx = _Factory.CreateContext()) using (var ctx = _Factory.CreateContext())
{ {
return (await ctx.PairedSINData return (await ctx.PairedSINData
@ -43,6 +45,46 @@ namespace BTCPayServer.Authentication
} }
} }
public async Task<String> 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<string[]> 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) private BitTokenEntity CreateTokenEntity(PairedSINData data)
{ {
return new BitTokenEntity() return new BitTokenEntity()

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework> <TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.1.88</Version> <Version>1.0.1.96</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn> <NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -31,18 +31,19 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BuildBundlerMinifier" Version="2.6.375" /> <PackageReference Include="BuildBundlerMinifier" Version="2.6.375" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.4.0" />
<PackageReference Include="Hangfire" Version="1.6.19" /> <PackageReference Include="Hangfire" Version="1.6.19" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" /> <PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" /> <PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
<PackageReference Include="LedgerWallet" Version="1.0.1.35" /> <PackageReference Include="LedgerWallet" Version="1.0.1.36" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.6.1" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.6.1" />
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="1.0.1" /> <PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="1.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" /> <PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" /> <PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
<PackageReference Include="NBitcoin" Version="4.1.1" /> <PackageReference Include="NBitcoin" Version="4.1.1.3" />
<PackageReference Include="NBitpayClient" Version="1.0.0.18" /> <PackageReference Include="NBitpayClient" Version="1.0.0.18" />
<PackageReference Include="DBreeze" Version="1.87.0" /> <PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2" /> <PackageReference Include="NBXplorer.Client" Version="1.0.2.3" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" /> <PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" /> <PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" /> <PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
@ -112,7 +113,6 @@
<ItemGroup> <ItemGroup>
<Folder Include="Build\" /> <Folder Include="Build\" />
<Folder Include="wwwroot\main\js\" />
<Folder Include="wwwroot\vendor\clipboard.js\" /> <Folder Include="wwwroot\vendor\clipboard.js\" />
</ItemGroup> </ItemGroup>

View File

@ -12,6 +12,7 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
[BitpayAPIConstraint]
public class AccessTokenController : Controller public class AccessTokenController : Controller
{ {
TokenRepository _TokenRepository; TokenRepository _TokenRepository;
@ -23,7 +24,7 @@ namespace BTCPayServer.Controllers
[Route("tokens")] [Route("tokens")]
public async Task<GetTokensResponse> Tokens() public async Task<GetTokensResponse> Tokens()
{ {
var tokens = await _TokenRepository.GetTokens(this.GetBitIdentity().SIN); var tokens = await _TokenRepository.GetTokens(this.User.GetSIN());
return new GetTokensResponse(tokens); return new GetTokensResponse(tokens);
} }
@ -51,7 +52,7 @@ namespace BTCPayServer.Controllers
} }
else else
{ {
var sin = this.GetBitIdentity(false)?.SIN ?? request.Id; var sin = this.User.GetSIN() ?? request.Id;
if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id)) if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
throw new BitpayHttpException(400, "'id' property is required, alternatively, use BitId"); throw new BitpayHttpException(400, "'id' property is required, alternatively, use BitId");

View File

@ -16,10 +16,11 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Mails; using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using BTCPayServer.Logging; using BTCPayServer.Logging;
using BTCPayServer.Security;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
[Authorize] [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[Route("[controller]/[action]")] [Route("[controller]/[action]")]
public class AccountController : Controller public class AccountController : Controller
{ {

View File

@ -42,10 +42,12 @@ namespace BTCPayServer.Controllers
" price: 15\n\n" + " price: 15\n\n" +
"tshirt:\n" + "tshirt:\n" +
" price: 25"; " price: 25";
ShowCustomAmount = true;
} }
public string Title { get; set; } public string Title { get; set; }
public string Currency { get; set; } public string Currency { get; set; }
public string Template { get; set; } public string Template { get; set; }
public bool ShowCustomAmount { get; set; }
} }
[HttpGet] [HttpGet]
@ -57,7 +59,7 @@ namespace BTCPayServer.Controllers
return NotFound(); return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>(); var settings = app.GetSettings<PointOfSaleSettings>();
return View(new UpdatePointOfSaleViewModel() { Title = settings.Title, Currency = settings.Currency, Template = settings.Template }); return View(new UpdatePointOfSaleViewModel() { Title = settings.Title, ShowCustomAmount = settings.ShowCustomAmount, Currency = settings.Currency, Template = settings.Template });
} }
[HttpPost] [HttpPost]
[Route("{appId}/settings/pos")] [Route("{appId}/settings/pos")]
@ -83,6 +85,7 @@ namespace BTCPayServer.Controllers
app.SetSettings(new PointOfSaleSettings() app.SetSettings(new PointOfSaleSettings()
{ {
Title = vm.Title, Title = vm.Title,
ShowCustomAmount = vm.ShowCustomAmount,
Currency = vm.Currency.ToUpperInvariant(), Currency = vm.Currency.ToUpperInvariant(),
Template = vm.Template Template = vm.Template
}); });
@ -99,9 +102,13 @@ namespace BTCPayServer.Controllers
if (app == null) if (app == null)
return NotFound(); return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>(); var settings = app.GetSettings<PointOfSaleSettings>();
var currency = _Currencies.GetCurrencyData(settings.Currency);
double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility));
return View(new ViewPointOfSaleViewModel() return View(new ViewPointOfSaleViewModel()
{ {
Title = settings.Title, Title = settings.Title,
Step = step.ToString(CultureInfo.InvariantCulture),
ShowCustomAmount = settings.ShowCustomAmount,
Items = Parse(settings.Template, settings.Currency) Items = Parse(settings.Template, settings.Currency)
}); });
} }
@ -155,23 +162,43 @@ namespace BTCPayServer.Controllers
[HttpPost] [HttpPost]
[Route("{appId}/pos")] [Route("{appId}/pos")]
public async Task<IActionResult> ViewPointOfSale(string appId, string choiceKey) public async Task<IActionResult> ViewPointOfSale(string appId, double amount, string choiceKey)
{ {
var app = await GetApp(appId, AppType.PointOfSale); var app = await GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
if (app == null) if (app == null)
return NotFound(); return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>(); var settings = app.GetSettings<PointOfSaleSettings>();
var choices = Parse(settings.Template, settings.Currency); if(string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
var choice = choices.FirstOrDefault(c => c.Id == choiceKey); {
if (choice == null) return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
return NotFound(); }
string title = null;
double price = 0.0;
if (!string.IsNullOrEmpty(choiceKey))
{
var choices = Parse(settings.Template, settings.Currency);
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
title = choice.Title;
price = (double)choice.Price.Value;
}
else
{
price = amount;
title = settings.Title;
}
var store = await GetStore(app); var store = await GetStore(app);
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice() var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
{ {
ItemDesc = choice.Title, ItemDesc = title,
Currency = settings.Currency, Currency = settings.Currency,
Price = (double)choice.Price.Value, Price = price,
}, store, HttpContext.Request.GetAbsoluteRoot()); }, store, HttpContext.Request.GetAbsoluteRoot());
return Redirect(invoice.Data.Url); return Redirect(invoice.Data.Url);
} }

View File

@ -140,6 +140,8 @@ namespace BTCPayServer.Controllers
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner) .Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId)) .SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (app == null)
return null;
if (type != null && type.Value.ToString() != app.AppType) if (type != null && type.Value.ToString() != app.AppType)
return null; return null;
return app; return app;

View File

@ -22,20 +22,14 @@ namespace BTCPayServer.Controllers
{ {
private InvoiceController _InvoiceController; private InvoiceController _InvoiceController;
private InvoiceRepository _InvoiceRepository; private InvoiceRepository _InvoiceRepository;
private TokenRepository _TokenRepository;
private StoreRepository _StoreRepository;
private BTCPayNetworkProvider _NetworkProvider; private BTCPayNetworkProvider _NetworkProvider;
public InvoiceControllerAPI(InvoiceController invoiceController, public InvoiceControllerAPI(InvoiceController invoiceController,
InvoiceRepository invoceRepository, InvoiceRepository invoceRepository,
TokenRepository tokenRepository,
StoreRepository storeRepository,
BTCPayNetworkProvider networkProvider) BTCPayNetworkProvider networkProvider)
{ {
this._InvoiceController = invoiceController; this._InvoiceController = invoiceController;
this._InvoiceRepository = invoceRepository; this._InvoiceRepository = invoceRepository;
this._TokenRepository = tokenRepository;
this._StoreRepository = storeRepository;
this._NetworkProvider = networkProvider; this._NetworkProvider = networkProvider;
} }
@ -44,21 +38,16 @@ namespace BTCPayServer.Controllers
[MediaTypeConstraint("application/json")] [MediaTypeConstraint("application/json")]
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice) public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
{ {
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, invoice.Token); return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot());
var store = await FindStore(bitToken);
return await _InvoiceController.CreateInvoiceCore(invoice, store, HttpContext.Request.GetAbsoluteRoot());
} }
[HttpGet] [HttpGet]
[Route("invoices/{id}")] [Route("invoices/{id}")]
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token) public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
{ {
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token); var invoice = await _InvoiceRepository.GetInvoice(HttpContext.GetStoreData().Id, id);
var store = await FindStore(bitToken);
var invoice = await _InvoiceRepository.GetInvoice(store.Id, id);
if (invoice == null) if (invoice == null)
throw new BitpayHttpException(404, "Object not found"); throw new BitpayHttpException(404, "Object not found");
var resp = invoice.EntityToDTO(_NetworkProvider); var resp = invoice.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp); return new DataWrapper<InvoiceResponse>(resp);
} }
@ -77,8 +66,7 @@ namespace BTCPayServer.Controllers
{ {
if (dateEnd != null) if (dateEnd != null)
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
var store = await FindStore(bitToken);
var query = new InvoiceQuery() var query = new InvoiceQuery()
{ {
Count = limit, Count = limit,
@ -87,55 +75,14 @@ namespace BTCPayServer.Controllers
StartDate = dateStart, StartDate = dateStart,
OrderId = orderId, OrderId = orderId,
ItemCode = itemCode, ItemCode = itemCode,
Status = status, Status = status == null ? null : new[] { status },
StoreId = store.Id StoreId = new[] { this.HttpContext.GetStoreData().Id }
}; };
var entities = (await _InvoiceRepository.GetInvoices(query)) var entities = (await _InvoiceRepository.GetInvoices(query))
.Select((o) => o.EntityToDTO(_NetworkProvider)).ToArray(); .Select((o) => o.EntityToDTO(_NetworkProvider)).ToArray();
return DataWrapper.Create(entities); return DataWrapper.Create(entities);
} }
private async Task<BitTokenEntity> CheckTokenPermissionAsync(Facade facade, string expectedToken)
{
if (facade == null)
throw new ArgumentNullException(nameof(facade));
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.Facade).Concat(new[] { "user" }).FirstOrDefault()}` facade");
}
return actualToken;
}
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
{
if (token.Facade == Facade.Merchant.ToString())
{
yield return token.Clone(Facade.User);
yield return token.Clone(Facade.PointOfSale);
}
if (token.Facade == Facade.PointOfSale.ToString())
{
yield return token.Clone(Facade.User);
}
yield return token;
}
private async Task<StoreData> FindStore(BitTokenEntity bitToken)
{
var store = await _StoreRepository.FindStore(bitToken.StoreId);
if (store == null)
throw new BitpayHttpException(401, "Unknown store");
return store;
}
} }
} }

View File

@ -22,6 +22,7 @@ using BTCPayServer.Events;
using NBXplorer; using NBXplorer;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
@ -232,6 +233,7 @@ namespace BTCPayServer.Controllers
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(), OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
BtcDue = accounting.Due.ToString(), BtcDue = accounting.Due.ToString(),
CustomerEmail = invoice.RefundMail, CustomerEmail = invoice.RefundMail,
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds), ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds, MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes, MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
@ -354,7 +356,7 @@ namespace BTCPayServer.Controllers
[HttpGet] [HttpGet]
[Route("invoices")] [Route("invoices")]
[Authorize(AuthenticationSchemes = "Identity.Application")] [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)] [BitpayAPIConstraint(false)]
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50) public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
{ {
@ -366,8 +368,8 @@ namespace BTCPayServer.Controllers
Count = count, Count = count,
Skip = skip, Skip = skip,
UserId = GetUserId(), UserId = GetUserId(),
Status = filterString.Filters.TryGet("status"), Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
StoreId = filterString.Filters.TryGet("storeid") StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
})) }))
{ {
model.SearchTerm = searchTerm; model.SearchTerm = searchTerm;
@ -389,11 +391,11 @@ namespace BTCPayServer.Controllers
[HttpGet] [HttpGet]
[Route("invoices/create")] [Route("invoices/create")]
[Authorize(AuthenticationSchemes = "Identity.Application")] [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)] [BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice() public async Task<IActionResult> CreateInvoice()
{ {
var stores = await GetStores(GetUserId()); var stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()), nameof(StoreData.Id), nameof(StoreData.StoreName), null);
if (stores.Count() == 0) if (stores.Count() == 0)
{ {
StatusMessage = "Error: You need to create at least one store before creating a transaction"; StatusMessage = "Error: You need to create at least one store before creating a transaction";
@ -404,18 +406,23 @@ namespace BTCPayServer.Controllers
[HttpPost] [HttpPost]
[Route("invoices/create")] [Route("invoices/create")]
[Authorize(AuthenticationSchemes = "Identity.Application")] [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)] [BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model) public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
{ {
model.Stores = await GetStores(GetUserId(), model.StoreId); var stores = await _StoreRepository.GetStoresByUserId(GetUserId());
model.Stores = new SelectList(stores, nameof(StoreData.Id), nameof(StoreData.StoreName), model.StoreId);
var store = stores.FirstOrDefault(s => s.Id == model.StoreId);
if(store == null)
{
ModelState.AddModelError(nameof(model.StoreId), "Store not found");
}
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
return View(model); return View(model);
} }
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
StatusMessage = null; StatusMessage = null;
if (store.Role != StoreRoles.Owner) if (!store.HasClaim(Policies.CanModifyStoreSettings.Key))
{ {
ModelState.AddModelError(nameof(model.StoreId), "You need to be owner of this store to create an invoice"); ModelState.AddModelError(nameof(model.StoreId), "You need to be owner of this store to create an invoice");
return View(model); return View(model);
@ -460,13 +467,8 @@ namespace BTCPayServer.Controllers
} }
} }
private async Task<SelectList> GetStores(string userId, string storeId = null)
{
return new SelectList(await _StoreRepository.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
}
[HttpPost] [HttpPost]
[Authorize(AuthenticationSchemes = "Identity.Application")] [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)] [BitpayAPIConstraint(false)]
public IActionResult SearchInvoice(InvoicesModel invoices) public IActionResult SearchInvoice(InvoicesModel invoices)
{ {
@ -480,7 +482,7 @@ namespace BTCPayServer.Controllers
[HttpPost] [HttpPost]
[Route("invoices/invalidatepaid")] [Route("invoices/invalidatepaid")]
[Authorize(AuthenticationSchemes = "Identity.Application")] [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)] [BitpayAPIConstraint(false)]
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId) public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
{ {

View File

@ -21,10 +21,11 @@ using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets; using BTCPayServer.Services.Wallets;
using BTCPayServer.Services.Mails; using BTCPayServer.Services.Mails;
using System.Globalization; using System.Globalization;
using BTCPayServer.Security;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
[Authorize] [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[Route("[controller]/[action]")] [Route("[controller]/[action]")]
public class ManageController : Controller public class ManageController : Controller
{ {

View File

@ -19,23 +19,20 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
[Authorize(Roles = Roles.ServerAdmin)] [Authorize(Policy = BTCPayServer.Security.Policies.CanModifyServerSettings.Key)]
public class ServerController : Controller public class ServerController : Controller
{ {
private UserManager<ApplicationUser> _UserManager; private UserManager<ApplicationUser> _UserManager;
SettingsRepository _SettingsRepository; SettingsRepository _SettingsRepository;
private IRateProviderFactory _RateProviderFactory; private IRateProviderFactory _RateProviderFactory;
private CssThemeManager _CssThemeManager;
public ServerController(UserManager<ApplicationUser> userManager, public ServerController(UserManager<ApplicationUser> userManager,
IRateProviderFactory rateProviderFactory, IRateProviderFactory rateProviderFactory,
SettingsRepository settingsRepository, SettingsRepository settingsRepository)
CssThemeManager cssThemeManager)
{ {
_UserManager = userManager; _UserManager = userManager;
_SettingsRepository = settingsRepository; _SettingsRepository = settingsRepository;
_RateProviderFactory = rateProviderFactory; _RateProviderFactory = rateProviderFactory;
_CssThemeManager = cssThemeManager;
} }
[Route("server/rates")] [Route("server/rates")]
@ -234,9 +231,6 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> Theme(ThemeSettings settings) public async Task<IActionResult> Theme(ThemeSettings settings)
{ {
await _SettingsRepository.UpdateSetting(settings); await _SettingsRepository.UpdateSetting(settings);
// TODO: remove controller/class-level property and have only reference to
// CssThemeManager here in this method
_CssThemeManager.Update(settings);
TempData["StatusMessage"] = "Theme settings updated successfully"; TempData["StatusMessage"] = "Theme settings updated successfully";
return View(settings); return View(settings);
} }

View File

@ -21,9 +21,9 @@ namespace BTCPayServer.Controllers
{ {
[HttpGet] [HttpGet]
[Route("{storeId}/derivations/{cryptoCode}")] [Route("{storeId}/derivations/{cryptoCode}")]
public async Task<IActionResult> AddDerivationScheme(string storeId, string cryptoCode) public IActionResult AddDerivationScheme(string storeId, string cryptoCode)
{ {
var store = await _Repo.FindStore(storeId, GetUserId()); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
return NotFound(); return NotFound();
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode); var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
@ -60,7 +60,7 @@ namespace BTCPayServer.Controllers
{ {
vm.ServerUrl = GetStoreUrl(storeId); vm.ServerUrl = GetStoreUrl(storeId);
vm.CryptoCode = cryptoCode; vm.CryptoCode = cryptoCode;
var store = await _Repo.FindStore(storeId, GetUserId()); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
return NotFound(); return NotFound();
@ -188,7 +188,7 @@ namespace BTCPayServer.Controllers
{ {
if (!HttpContext.WebSockets.IsWebSocketRequest) if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound(); return NotFound();
var store = await _Repo.FindStore(storeId, GetUserId()); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
return NotFound(); return NotFound();

View File

@ -19,9 +19,9 @@ namespace BTCPayServer.Controllers
[HttpGet] [HttpGet]
[Route("{storeId}/lightning/{cryptoCode}")] [Route("{storeId}/lightning/{cryptoCode}")]
public async Task<IActionResult> AddLightningNode(string storeId, string cryptoCode) public IActionResult AddLightningNode(string storeId, string cryptoCode)
{ {
var store = await _Repo.FindStore(storeId, GetUserId()); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
return NotFound(); return NotFound();
LightningNodeViewModel vm = new LightningNodeViewModel(); LightningNodeViewModel vm = new LightningNodeViewModel();
@ -59,7 +59,7 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode) public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
{ {
vm.CryptoCode = cryptoCode; vm.CryptoCode = cryptoCode;
var store = await _Repo.FindStore(storeId, GetUserId()); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
return NotFound(); return NotFound();
var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode); var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode);

View File

@ -4,6 +4,7 @@ using BTCPayServer.Data;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Models; using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
@ -27,8 +28,8 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
[Route("stores")] [Route("stores")]
[Authorize(AuthenticationSchemes = "Identity.Application")] [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[Authorize(Policy = StorePolicies.OwnStore)] [Authorize(Policy = Policies.CanModifyStoreSettings.Key)]
[AutoValidateAntiforgeryToken] [AutoValidateAntiforgeryToken]
public partial class StoresController : Controller public partial class StoresController : Controller
{ {
@ -93,13 +94,10 @@ namespace BTCPayServer.Controllers
[HttpGet] [HttpGet]
[Route("{storeId}/wallet/{cryptoCode}")] [Route("{storeId}/wallet/{cryptoCode}")]
public async Task<IActionResult> Wallet(string storeId, string cryptoCode) public IActionResult Wallet(string cryptoCode)
{ {
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
WalletModel model = new WalletModel(); WalletModel model = new WalletModel();
model.ServerUrl = GetStoreUrl(storeId); model.ServerUrl = GetStoreUrl(StoreData.Id);
model.CryptoCurrency = cryptoCode; model.CryptoCurrency = cryptoCode;
return View(model); return View(model);
} }
@ -111,17 +109,17 @@ namespace BTCPayServer.Controllers
[HttpGet] [HttpGet]
[Route("{storeId}/users")] [Route("{storeId}/users")]
public async Task<IActionResult> StoreUsers(string storeId) public async Task<IActionResult> StoreUsers()
{ {
StoreUsersViewModel vm = new StoreUsersViewModel(); StoreUsersViewModel vm = new StoreUsersViewModel();
await FillUsers(storeId, vm); await FillUsers(vm);
return View(vm); return View(vm);
} }
private async Task FillUsers(string storeId, StoreUsersViewModel vm) private async Task FillUsers(StoreUsersViewModel vm)
{ {
var users = await _Repo.GetStoreUsers(storeId); var users = await _Repo.GetStoreUsers(StoreData.Id);
vm.StoreId = storeId; vm.StoreId = StoreData.Id;
vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel() vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel()
{ {
Email = u.Email, Email = u.Email,
@ -130,11 +128,20 @@ namespace BTCPayServer.Controllers
}).ToList(); }).ToList();
} }
public StoreData StoreData
{
get
{
return this.HttpContext.GetStoreData();
}
}
[HttpPost] [HttpPost]
[Route("{storeId}/users")] [Route("{storeId}/users")]
public async Task<IActionResult> StoreUsers(string storeId, StoreUsersViewModel vm) public async Task<IActionResult> StoreUsers(StoreUsersViewModel vm)
{ {
await FillUsers(storeId, vm); await FillUsers(vm);
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
return View(vm); return View(vm);
@ -150,7 +157,7 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(nameof(vm.Role), "Invalid role"); ModelState.AddModelError(nameof(vm.Role), "Invalid role");
return View(vm); return View(vm);
} }
if (!await _Repo.AddStoreUser(storeId, user.Id, vm.Role)) if (!await _Repo.AddStoreUser(StoreData.Id, user.Id, vm.Role))
{ {
ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store"); ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store");
return View(vm); return View(vm);
@ -161,19 +168,16 @@ namespace BTCPayServer.Controllers
[HttpGet] [HttpGet]
[Route("{storeId}/users/{userId}/delete")] [Route("{storeId}/users/{userId}/delete")]
public async Task<IActionResult> DeleteStoreUser(string storeId, string userId) public async Task<IActionResult> DeleteStoreUser(string userId)
{ {
StoreUsersViewModel vm = new StoreUsersViewModel(); StoreUsersViewModel vm = new StoreUsersViewModel();
var store = await _Repo.FindStore(storeId, userId);
if (store == null)
return NotFound();
var user = await _UserManager.FindByIdAsync(userId); var user = await _UserManager.FindByIdAsync(userId);
if (user == null) if (user == null)
return NotFound(); return NotFound();
return View("Confirm", new ConfirmModel() return View("Confirm", new ConfirmModel()
{ {
Title = $"Remove store user", Title = $"Remove store user",
Description = $"Are you sure to remove access to remove {store.Role} access to {user.Email}?", Description = $"Are you sure to remove access to remove access to {user.Email}?",
Action = "Delete" Action = "Delete"
}); });
} }
@ -189,18 +193,16 @@ namespace BTCPayServer.Controllers
[HttpGet] [HttpGet]
[Route("{storeId}/checkout")] [Route("{storeId}/checkout")]
public async Task<IActionResult> CheckoutExperience(string storeId) public IActionResult CheckoutExperience()
{ {
var store = await _Repo.FindStore(storeId, GetUserId()); var storeBlob = StoreData.GetStoreBlob();
if (store == null)
return NotFound();
var storeBlob = store.GetStoreBlob();
var vm = new CheckoutExperienceViewModel(); var vm = new CheckoutExperienceViewModel();
vm.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto()); vm.SetCryptoCurrencies(_ExplorerProvider, StoreData.GetDefaultCrypto());
vm.SetLanguages(_LangService, storeBlob.DefaultLang); vm.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? ""; vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? ""; vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
vm.AllowCoinConversion = storeBlob.AllowCoinConversion; vm.AllowCoinConversion = storeBlob.AllowCoinConversion;
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri; vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri; vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
return View(vm); return View(vm);
@ -208,7 +210,7 @@ namespace BTCPayServer.Controllers
[HttpPost] [HttpPost]
[Route("{storeId}/checkout")] [Route("{storeId}/checkout")]
public async Task<IActionResult> CheckoutExperience(string storeId, CheckoutExperienceViewModel model) public async Task<IActionResult> CheckoutExperience(CheckoutExperienceViewModel model)
{ {
CurrencyValue lightningMaxValue = null; CurrencyValue lightningMaxValue = null;
if (!string.IsNullOrWhiteSpace(model.LightningMaxValue)) if (!string.IsNullOrWhiteSpace(model.LightningMaxValue))
@ -227,16 +229,12 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(nameof(model.OnChainMinValue), "Invalid on chain min value"); ModelState.AddModelError(nameof(model.OnChainMinValue), "Invalid on chain min value");
} }
} }
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
bool needUpdate = false; bool needUpdate = false;
var blob = store.GetStoreBlob(); var blob = StoreData.GetStoreBlob();
if (store.GetDefaultCrypto() != model.DefaultCryptoCurrency) if (StoreData.GetDefaultCrypto() != model.DefaultCryptoCurrency)
{ {
needUpdate = true; needUpdate = true;
store.SetDefaultCrypto(model.DefaultCryptoCurrency); StoreData.SetDefaultCrypto(model.DefaultCryptoCurrency);
} }
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency); model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
model.SetLanguages(_LangService, model.DefaultLang); model.SetLanguages(_LangService, model.DefaultLang);
@ -247,31 +245,32 @@ namespace BTCPayServer.Controllers
} }
blob.DefaultLang = model.DefaultLang; blob.DefaultLang = model.DefaultLang;
blob.AllowCoinConversion = model.AllowCoinConversion; blob.AllowCoinConversion = model.AllowCoinConversion;
blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.LightningMaxValue = lightningMaxValue; blob.LightningMaxValue = lightningMaxValue;
blob.OnChainMinValue = onchainMinValue; blob.OnChainMinValue = onchainMinValue;
blob.CustomLogo = string.IsNullOrWhiteSpace(model.CustomLogo) ? null : new Uri(model.CustomLogo, UriKind.Absolute); blob.CustomLogo = string.IsNullOrWhiteSpace(model.CustomLogo) ? null : new Uri(model.CustomLogo, UriKind.Absolute);
blob.CustomCSS = string.IsNullOrWhiteSpace(model.CustomCSS) ? null : new Uri(model.CustomCSS, UriKind.Absolute); blob.CustomCSS = string.IsNullOrWhiteSpace(model.CustomCSS) ? null : new Uri(model.CustomCSS, UriKind.Absolute);
if (store.SetStoreBlob(blob)) if (StoreData.SetStoreBlob(blob))
{ {
needUpdate = true; needUpdate = true;
} }
if (needUpdate) if (needUpdate)
{ {
await _Repo.UpdateStore(store); await _Repo.UpdateStore(StoreData);
StatusMessage = "Store successfully updated"; StatusMessage = "Store successfully updated";
} }
return RedirectToAction(nameof(CheckoutExperience), new return RedirectToAction(nameof(CheckoutExperience), new
{ {
storeId = storeId storeId = StoreData.Id
}); });
} }
[HttpGet] [HttpGet]
[Route("{storeId}")] [Route("{storeId}")]
public async Task<IActionResult> UpdateStore(string storeId) public IActionResult UpdateStore(string storeId)
{ {
var store = await _Repo.FindStore(storeId, GetUserId()); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
return NotFound(); return NotFound();
@ -327,7 +326,7 @@ namespace BTCPayServer.Controllers
[HttpPost] [HttpPost]
[Route("{storeId}")] [Route("{storeId}")]
public async Task<IActionResult> UpdateStore(string storeId, StoreViewModel model) public async Task<IActionResult> UpdateStore(StoreViewModel model)
{ {
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange); model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
if (!ModelState.IsValid) if (!ModelState.IsValid)
@ -336,29 +335,26 @@ namespace BTCPayServer.Controllers
} }
if (model.PreferredExchange != null) if (model.PreferredExchange != null)
model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant(); model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant();
var store = await _Repo.FindStore(storeId, GetUserId()); AddPaymentMethods(StoreData, model);
if (store == null)
return NotFound();
AddPaymentMethods(store, model);
bool needUpdate = false; bool needUpdate = false;
if (store.SpeedPolicy != model.SpeedPolicy) if (StoreData.SpeedPolicy != model.SpeedPolicy)
{ {
needUpdate = true; needUpdate = true;
store.SpeedPolicy = model.SpeedPolicy; StoreData.SpeedPolicy = model.SpeedPolicy;
} }
if (store.StoreName != model.StoreName) if (StoreData.StoreName != model.StoreName)
{ {
needUpdate = true; needUpdate = true;
store.StoreName = model.StoreName; StoreData.StoreName = model.StoreName;
} }
if (store.StoreWebsite != model.StoreWebsite) if (StoreData.StoreWebsite != model.StoreWebsite)
{ {
needUpdate = true; needUpdate = true;
store.StoreWebsite = model.StoreWebsite; StoreData.StoreWebsite = model.StoreWebsite;
} }
var blob = store.GetStoreBlob(); var blob = StoreData.GetStoreBlob();
blob.NetworkFeeDisabled = !model.NetworkFee; blob.NetworkFeeDisabled = !model.NetworkFee;
blob.MonitoringExpiration = model.MonitoringExpiration; blob.MonitoringExpiration = model.MonitoringExpiration;
blob.InvoiceExpiration = model.InvoiceExpiration; blob.InvoiceExpiration = model.InvoiceExpiration;
@ -369,7 +365,7 @@ namespace BTCPayServer.Controllers
blob.SetRateMultiplier(model.RateMultiplier); blob.SetRateMultiplier(model.RateMultiplier);
if (store.SetStoreBlob(blob)) if (StoreData.SetStoreBlob(blob))
{ {
needUpdate = true; needUpdate = true;
} }
@ -386,19 +382,22 @@ namespace BTCPayServer.Controllers
if (needUpdate) if (needUpdate)
{ {
await _Repo.UpdateStore(store); await _Repo.UpdateStore(StoreData);
StatusMessage = "Store successfully updated"; StatusMessage = "Store successfully updated";
} }
return RedirectToAction(nameof(UpdateStore), new return RedirectToAction(nameof(UpdateStore), new
{ {
storeId = storeId storeId = StoreData.Id
}); });
} }
private (String DisplayName, String Name)[] GetSupportedExchanges() private (String DisplayName, String Name)[] GetSupportedExchanges()
{ {
return new[] { ("Coin Average", "coinaverage") }.Concat(_CoinAverage.AvailableExchanges).ToArray(); return new[] { ("Coin Average", "coinaverage") }
.Concat(_CoinAverage.AvailableExchanges)
.OrderBy(s => s.Item1, StringComparer.OrdinalIgnoreCase)
.ToArray();
} }
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network) private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
@ -410,10 +409,10 @@ namespace BTCPayServer.Controllers
[HttpGet] [HttpGet]
[Route("{storeId}/Tokens")] [Route("{storeId}/Tokens")]
public async Task<IActionResult> ListTokens(string storeId) public async Task<IActionResult> ListTokens()
{ {
var model = new TokensViewModel(); var model = new TokensViewModel();
var tokens = await _TokenRepository.GetTokensByStoreIdAsync(storeId); var tokens = await _TokenRepository.GetTokensByStoreIdAsync(StoreData.Id);
model.StatusMessage = StatusMessage; model.StatusMessage = StatusMessage;
model.Tokens = tokens.Select(t => new TokenViewModel() model.Tokens = tokens.Select(t => new TokenViewModel()
{ {
@ -422,30 +421,43 @@ namespace BTCPayServer.Controllers
SIN = t.SIN, SIN = t.SIN,
Id = t.Value Id = t.Value
}).ToArray(); }).ToArray();
model.ApiKey = (await _TokenRepository.GetLegacyAPIKeys(StoreData.Id)).FirstOrDefault();
if (model.ApiKey == null)
model.EncodedApiKey = "*API Key*";
else
model.EncodedApiKey = Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(model.ApiKey));
return View(model); return View(model);
} }
[HttpPost] [HttpPost]
[Route("/api-tokens")] [Route("/api-tokens")]
[Route("{storeId}/Tokens/Create")] [Route("{storeId}/Tokens/Create")]
public async Task<IActionResult> CreateToken(string storeId, CreateTokenViewModel model) [AllowAnonymous]
public async Task<IActionResult> CreateToken(CreateTokenViewModel model)
{ {
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
return View(model); return View(model);
} }
model.Label = model.Label ?? String.Empty; model.Label = model.Label ?? String.Empty;
storeId = model.StoreId ?? storeId;
var userId = GetUserId(); var userId = GetUserId();
if (userId == null) if (userId == null)
return Unauthorized(); return Challenge(Policies.CookieAuthentication);
var store = await _Repo.FindStore(storeId, userId);
if (store == null) var store = StoreData;
return Unauthorized(); var storeId = StoreData?.Id;
if (store.Role != StoreRoles.Owner) if (storeId == null)
{ {
StatusMessage = "Error: You need to be owner of this store to request pairing codes"; storeId = model.StoreId;
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores"); store = await _Repo.FindStore(storeId, userId);
if (store == null)
return Challenge(Policies.CookieAuthentication);
}
if (!store.HasClaim(Policies.CanModifyStoreSettings.Key))
{
return Challenge(Policies.CookieAuthentication);
} }
var tokenRequest = new TokenRequest() var tokenRequest = new TokenRequest()
@ -486,11 +498,20 @@ namespace BTCPayServer.Controllers
[HttpGet] [HttpGet]
[Route("/api-tokens")] [Route("/api-tokens")]
[Route("{storeId}/Tokens/Create")] [Route("{storeId}/Tokens/Create")]
public async Task<IActionResult> CreateToken(string storeId) [AllowAnonymous]
public async Task<IActionResult> CreateToken()
{ {
var userId = GetUserId(); var userId = GetUserId();
if (string.IsNullOrWhiteSpace(userId)) if (string.IsNullOrWhiteSpace(userId))
return Unauthorized(); return Challenge(Policies.CookieAuthentication);
var storeId = StoreData?.Id;
if (StoreData != null)
{
if (!StoreData.HasClaim(Policies.CanModifyStoreSettings.Key))
{
return Challenge(Policies.CookieAuthentication);
}
}
var model = new CreateTokenViewModel(); var model = new CreateTokenViewModel();
model.Facade = "merchant"; model.Facade = "merchant";
ViewBag.HidePublicKey = storeId == null; ViewBag.HidePublicKey = storeId == null;
@ -499,20 +520,25 @@ namespace BTCPayServer.Controllers
model.StoreId = storeId; model.StoreId = storeId;
if (storeId == null) if (storeId == null)
{ {
model.Stores = new SelectList(await _Repo.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId); var stores = await _Repo.GetStoresByUserId(userId);
model.Stores = new SelectList(stores.Where(s => s.HasClaim(Policies.CanModifyStoreSettings.Key)), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
}
if (model.Stores.Count() == 0)
{
StatusMessage = "Error: You need to be owner of at least one store before pairing";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
} }
return View(model); return View(model);
} }
[HttpPost] [HttpPost]
[Route("{storeId}/Tokens/Delete")] [Route("{storeId}/Tokens/Delete")]
public async Task<IActionResult> DeleteToken(string storeId, string tokenId) public async Task<IActionResult> DeleteToken(string tokenId)
{ {
var token = await _TokenRepository.GetToken(tokenId); var token = await _TokenRepository.GetToken(tokenId);
if (token == null || if (token == null ||
token.StoreId != storeId || token.StoreId != StoreData.Id ||
!await _TokenRepository.DeleteToken(tokenId)) !await _TokenRepository.DeleteToken(tokenId))
StatusMessage = "Failure to revoke this token"; StatusMessage = "Failure to revoke this token";
else else
@ -520,11 +546,26 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(ListTokens)); return RedirectToAction(nameof(ListTokens));
} }
[HttpPost]
[Route("{storeId}/tokens/apikey")]
public async Task<IActionResult> GenerateAPIKey()
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
await _TokenRepository.GenerateLegacyAPIKey(StoreData.Id);
StatusMessage = "API Key re-generated";
return RedirectToAction(nameof(ListTokens));
}
[HttpGet] [HttpGet]
[Route("/api-access-request")] [Route("/api-access-request")]
[AllowAnonymous]
public async Task<IActionResult> RequestPairing(string pairingCode, string selectedStore = null) public async Task<IActionResult> RequestPairing(string pairingCode, string selectedStore = null)
{ {
var userId = GetUserId();
if (userId == null)
return Challenge(Policies.CookieAuthentication);
if (pairingCode == null) if (pairingCode == null)
return NotFound(); return NotFound();
var pairing = await _TokenRepository.GetPairingAsync(pairingCode); var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
@ -535,7 +576,7 @@ namespace BTCPayServer.Controllers
} }
else else
{ {
var stores = await _Repo.GetStoresByUserId(GetUserId()); var stores = await _Repo.GetStoresByUserId(userId);
return View(new PairingModel() return View(new PairingModel()
{ {
Id = pairing.Id, Id = pairing.Id,
@ -543,7 +584,7 @@ namespace BTCPayServer.Controllers
Label = pairing.Label, Label = pairing.Label,
SIN = pairing.SIN ?? "Server-Initiated Pairing", SIN = pairing.SIN ?? "Server-Initiated Pairing",
SelectedStore = selectedStore ?? stores.FirstOrDefault()?.Id, SelectedStore = selectedStore ?? stores.FirstOrDefault()?.Id,
Stores = stores.Select(s => new PairingModel.StoreViewModel() Stores = stores.Where(u => u.HasClaim(Policies.CanModifyStoreSettings.Key)).Select(s => new PairingModel.StoreViewModel()
{ {
Id = s.Id, Id = s.Id,
Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName
@ -554,19 +595,22 @@ namespace BTCPayServer.Controllers
[HttpPost] [HttpPost]
[Route("/api-access-request")] [Route("/api-access-request")]
[AllowAnonymous]
public async Task<IActionResult> Pair(string pairingCode, string selectedStore) public async Task<IActionResult> Pair(string pairingCode, string selectedStore)
{ {
if (pairingCode == null) if (pairingCode == null)
return NotFound(); return NotFound();
var store = await _Repo.FindStore(selectedStore, GetUserId()); var userId = GetUserId();
if (userId == null)
return Challenge(Policies.CookieAuthentication);
var store = await _Repo.FindStore(selectedStore, userId);
var pairing = await _TokenRepository.GetPairingAsync(pairingCode); var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
if (store == null || pairing == null) if (store == null || pairing == null)
return NotFound(); return NotFound();
if (store.Role != StoreRoles.Owner) if (!store.HasClaim(Policies.CanModifyStoreSettings.Key))
{ {
StatusMessage = "Error: You can't approve a pairing without being owner of the store"; return Challenge(Policies.CookieAuthentication);
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
} }
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id); var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
@ -592,6 +636,8 @@ namespace BTCPayServer.Controllers
private string GetUserId() private string GetUserId()
{ {
if (User.Identity.AuthenticationType != Policies.CookieAuthentication)
return null;
return _UserManager.GetUserId(User); return _UserManager.GetUserId(User);
} }
} }

View File

@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Models; using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets; using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -15,7 +16,7 @@ using NBXplorer.DerivationStrategy;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
[Route("stores")] [Route("stores")]
[Authorize(AuthenticationSchemes = "Identity.Application")] [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[AutoValidateAntiforgeryToken] [AutoValidateAntiforgeryToken]
public partial class UserStoresController : Controller public partial class UserStoresController : Controller
{ {
@ -37,9 +38,9 @@ namespace BTCPayServer.Controllers
} }
[HttpGet] [HttpGet]
[Route("{storeId}/delete")] [Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStore(string storeId) public IActionResult DeleteStore(string storeId)
{ {
var store = await _Repo.FindStore(storeId, GetUserId()); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
return NotFound(); return NotFound();
return View("Confirm", new ConfirmModel() return View("Confirm", new ConfirmModel()
@ -67,7 +68,7 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> DeleteStorePost(string storeId) public async Task<IActionResult> DeleteStorePost(string storeId)
{ {
var userId = GetUserId(); var userId = GetUserId();
var store = await _Repo.FindStore(storeId, GetUserId()); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
return NotFound(); return NotFound();
await _Repo.RemoveStore(storeId, userId); await _Repo.RemoveStore(storeId, userId);
@ -102,8 +103,8 @@ namespace BTCPayServer.Controllers
Id = store.Id, Id = store.Id,
Name = store.StoreName, Name = store.StoreName,
WebSite = store.StoreWebsite, WebSite = store.StoreWebsite,
IsOwner = store.Role == StoreRoles.Owner, IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key),
Balances = store.Role == StoreRoles.Owner ? balances[i].Select(t => t.Result).ToArray() : Array.Empty<string>() Balances = store.HasClaim(Policies.CanModifyStoreSettings.Key) ? balances[i].Select(t => t.Result).ToArray() : Array.Empty<string>()
}); });
} }
return View(result); return View(result);

View File

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

View File

@ -86,6 +86,11 @@ namespace BTCPayServer.Data
get; set; get; set;
} }
public DbSet<APIKeyData> ApiKeys
{
get; set;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {
var isConfigured = optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().Any(); var isConfigured = optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().Any();
@ -112,6 +117,8 @@ namespace BTCPayServer.Data
t.StoreDataId t.StoreDataId
}); });
builder.Entity<APIKeyData>()
.HasIndex(o => o.StoreId);
builder.Entity<AppData>() builder.Entity<AppData>()
.HasOne(a => a.StoreData); .HasOne(a => a.StoreData);

View File

@ -14,6 +14,10 @@ using Newtonsoft.Json.Linq;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.JsonConverters; using BTCPayServer.JsonConverters;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Services;
using System.Security.Claims;
using BTCPayServer.Security;
namespace BTCPayServer.Data namespace BTCPayServer.Data
{ {
@ -120,7 +124,7 @@ namespace BTCPayServer.Data
} }
} }
if(!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain) if (!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain)
{ {
DerivationStrategy = null; DerivationStrategy = null;
} }
@ -151,10 +155,35 @@ namespace BTCPayServer.Data
} }
[NotMapped] [NotMapped]
[Obsolete]
public string Role public string Role
{ {
get; set; get; set;
} }
public Claim[] GetClaims()
{
List<Claim> claims = new List<Claim>();
#pragma warning disable CS0612 // Type or member is obsolete
var role = Role;
#pragma warning restore CS0612 // Type or member is obsolete
if (role == StoreRoles.Owner)
{
claims.Add(new Claim(Policies.CanModifyStoreSettings.Key, Id));
claims.Add(new Claim(Policies.CanUseStore.Key, Id));
}
if (role == StoreRoles.Guest)
{
claims.Add(new Claim(Policies.CanUseStore.Key, Id));
}
return claims.ToArray();
}
public bool HasClaim(string claim)
{
return GetClaims().Any(c => c.Type == claim);
}
public byte[] StoreBlob public byte[] StoreBlob
{ {
get; get;
@ -214,6 +243,7 @@ namespace BTCPayServer.Data
{ {
InvoiceExpiration = 15; InvoiceExpiration = 15;
MonitoringExpiration = 60; MonitoringExpiration = 60;
RequiresRefundEmail = true;
} }
public bool NetworkFeeDisabled public bool NetworkFeeDisabled
{ {
@ -223,6 +253,9 @@ namespace BTCPayServer.Data
{ {
get; set; get; set;
} }
public bool RequiresRefundEmail { get; set; }
public string DefaultLang { get; set; } public string DefaultLang { get; set; }
[DefaultValue(60)] [DefaultValue(60)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]

View File

@ -29,6 +29,8 @@ using Microsoft.AspNetCore.Identity;
using BTCPayServer.Models; using BTCPayServer.Models;
using System.Security.Claims; using System.Security.Claims;
using System.Globalization; using System.Globalization;
using BTCPayServer.Services;
using BTCPayServer.Data;
namespace BTCPayServer namespace BTCPayServer
{ {
@ -133,6 +135,14 @@ namespace BTCPayServer
request.PathBase.ToUriComponent()); request.PathBase.ToUriComponent());
} }
public static string GetAbsoluteUri(this HttpRequest request, string redirectUrl)
{
bool isRelative =
(redirectUrl.Length > 0 && redirectUrl[0] == '/')
|| !new Uri(redirectUrl, UriKind.RelativeOrAbsolute).IsAbsoluteUri;
return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl;
}
public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf) public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf)
{ {
services.Configure<BTCPayServerOptions>(o => services.Configure<BTCPayServerOptions>(o =>
@ -142,12 +152,45 @@ namespace BTCPayServer
return services; return services;
} }
public static string GetSIN(this ClaimsPrincipal principal)
public static BitIdentity GetBitIdentity(this Controller controller, bool throws = true)
{ {
if (!(controller.User.Identity is BitIdentity)) return principal.Claims.Where(c => c.Type == Claims.SIN).Select(c => c.Value).FirstOrDefault();
return throws ? throw new UnauthorizedAccessException("no-bitid") : (BitIdentity)null; }
return (BitIdentity)controller.User.Identity;
public static string GetStoreId(this ClaimsPrincipal principal)
{
return principal.Claims.Where(c => c.Type == Claims.OwnStore).Select(c => c.Value).FirstOrDefault();
}
public static void SetIsBitpayAPI(this HttpContext ctx, bool value)
{
NBitcoin.Extensions.TryAdd(ctx.Items, "IsBitpayAPI", value);
}
public static bool GetIsBitpayAPI(this HttpContext ctx)
{
return ctx.Items.TryGetValue("IsBitpayAPI", out object obj) &&
obj is bool b && b;
}
public static void SetBitpayAuth(this HttpContext ctx, (string Signature, String Id, String Authorization) value)
{
NBitcoin.Extensions.TryAdd(ctx.Items, "BitpayAuth", value);
}
public static (string Signature, String Id, String Authorization) GetBitpayAuth(this HttpContext ctx)
{
ctx.Items.TryGetValue("BitpayAuth", out object obj);
return ((string Signature, String Id, String Authorization))obj;
}
public static StoreData GetStoreData(this HttpContext ctx)
{
return ctx.Items.TryGet("BTCPAY.STOREDATA") as StoreData;
}
public static void SetStoreData(this HttpContext ctx, StoreData storeData)
{
ctx.Items["BTCPAY.STOREDATA"] = storeData;
} }
private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }; private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };

View File

@ -43,9 +43,7 @@ namespace BTCPayServer.Filters
public bool Accept(ActionConstraintContext context) public bool Accept(ActionConstraintContext context)
{ {
var hasVersion = context.RouteContext.HttpContext.Request.Headers["x-accept-version"].Where(h => h == "2.0.0").Any(); return context.RouteContext.HttpContext.GetIsBitpayAPI() == IsBitpayAPI;
var hasIdentity = context.RouteContext.HttpContext.Request.Headers["x-identity"].Any();
return (hasVersion || hasIdentity) == IsBitpayAPI;
} }
} }

View File

@ -0,0 +1,66 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Hosting;
using BTCPayServer.Logging;
using System.Runtime.CompilerServices;
using System.IO;
using System.Text;
namespace BTCPayServer.HostedServices
{
public abstract class BaseAsyncService : IHostedService
{
private CancellationTokenSource _Cts;
protected Task[] _Tasks;
public Task StartAsync(CancellationToken cancellationToken)
{
_Cts = new CancellationTokenSource();
_Tasks = InitializeTasks();
return Task.CompletedTask;
}
internal abstract Task[] InitializeTasks();
protected CancellationToken Cancellation
{
get { return _Cts.Token; }
}
protected async Task CreateLoopTask(Func<Task> act, [CallerMemberName]string caller = null)
{
await new SynchronizationContextRemover();
while (!_Cts.IsCancellationRequested)
{
try
{
await act();
}
catch (OperationCanceledException) when (_Cts.IsCancellationRequested)
{
}
catch (Exception ex)
{
Logs.PayServer.LogWarning(ex, caller + " failed");
try
{
await Task.Delay(TimeSpan.FromMinutes(1), _Cts.Token);
}
catch (OperationCanceledException) when (_Cts.IsCancellationRequested) { }
}
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_Cts.Cancel();
return Task.WhenAll(_Tasks);
}
}
}

View File

@ -16,47 +16,59 @@ namespace BTCPayServer.HostedServices
{ {
public class CssThemeManager public class CssThemeManager
{ {
public CssThemeManager(SettingsRepository settingsRepository)
{
Update(settingsRepository);
}
private async void Update(SettingsRepository settingsRepository)
{
var data = (await settingsRepository.GetSettingAsync<ThemeSettings>()) ?? new ThemeSettings();
Update(data);
}
public void Update(ThemeSettings data) public void Update(ThemeSettings data)
{ {
UpdateBootstrap(data.BootstrapCssUri); if (String.IsNullOrWhiteSpace(data.BootstrapCssUri))
UpdateCreativeStart(data.CreativeStartCssUri); _bootstrapUri = "/vendor/bootstrap4/css/bootstrap.css?v=" + DateTime.Now.Ticks;
} else
_bootstrapUri = data.BootstrapCssUri;
private string _bootstrapUri = "/vendor/bootstrap4/css/bootstrap.css?v=" + DateTime.Now.Ticks;
if (String.IsNullOrWhiteSpace(data.CreativeStartCssUri))
_creativeStartUri = "/vendor/bootstrap4-creativestart/creative.css?v=" + DateTime.Now.Ticks;
else
_creativeStartUri = data.CreativeStartCssUri;
}
private string _bootstrapUri;
public string BootstrapUri public string BootstrapUri
{ {
get { return _bootstrapUri; } get { return _bootstrapUri; }
} }
public void UpdateBootstrap(string newUri)
{
if (String.IsNullOrWhiteSpace(newUri))
_bootstrapUri = "/vendor/bootstrap4/css/bootstrap.css?v="+ DateTime.Now.Ticks;
else
_bootstrapUri = newUri;
}
private string _creativeStartUri = "/vendor/bootstrap4-creativestart/creative.css?v=" + DateTime.Now.Ticks; private string _creativeStartUri;
public string CreativeStartUri public string CreativeStartUri
{ {
get { return _creativeStartUri; } get { return _creativeStartUri; }
} }
public void UpdateCreativeStart(string newUri) }
public class CssThemeManagerHostedService : BaseAsyncService
{
private SettingsRepository _SettingsRepository;
private CssThemeManager _CssThemeManager;
public CssThemeManagerHostedService(SettingsRepository settingsRepository, CssThemeManager cssThemeManager)
{ {
if (String.IsNullOrWhiteSpace(newUri)) _SettingsRepository = settingsRepository;
_creativeStartUri = "/vendor/bootstrap4-creativestart/creative.css?v=" + DateTime.Now.Ticks; _CssThemeManager = cssThemeManager;
else }
_creativeStartUri = newUri;
internal override Task[] InitializeTasks()
{
return new[]
{
CreateLoopTask(ListenForThemeChanges)
};
}
async Task ListenForThemeChanges()
{
await new SynchronizationContextRemover();
var data = (await _SettingsRepository.GetSettingAsync<ThemeSettings>()) ?? new ThemeSettings();
_CssThemeManager.Update(data);
await _SettingsRepository.WaitSettingsChanged<ThemeSettings>(Cancellation);
} }
} }
} }

View File

@ -9,10 +9,12 @@ using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using BTCPayServer.Logging; using BTCPayServer.Logging;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.IO;
using System.Text;
namespace BTCPayServer.HostedServices namespace BTCPayServer.HostedServices
{ {
public class RatesHostedService : IHostedService public class RatesHostedService : BaseAsyncService
{ {
private SettingsRepository _SettingsRepository; private SettingsRepository _SettingsRepository;
private IRateProviderFactory _RateProviderFactory; private IRateProviderFactory _RateProviderFactory;
@ -26,76 +28,40 @@ namespace BTCPayServer.HostedServices
_coinAverageSettings = coinAverageSettings; _coinAverageSettings = coinAverageSettings;
} }
internal override Task[] InitializeTasks()
CancellationTokenSource _Cts = new CancellationTokenSource();
List<Task> _Tasks = new List<Task>();
public Task StartAsync(CancellationToken cancellationToken)
{ {
_Tasks.Add(RefreshCoinAverageSupportedExchanges(_Cts.Token)); return new[]
_Tasks.Add(RefreshCoinAverageSettings(_Cts.Token)); {
return Task.CompletedTask; CreateLoopTask(RefreshCoinAverageSupportedExchanges),
CreateLoopTask(RefreshCoinAverageSettings)
};
} }
async Task RefreshCoinAverageSupportedExchanges()
async Task Timer(Func<Task> act, CancellationToken cancellation, [CallerMemberName]string caller = null)
{ {
await new SynchronizationContextRemover(); await new SynchronizationContextRemover();
while (!cancellation.IsCancellationRequested) var tickers = await new CoinAverageRateProvider("BTC") { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
_coinAverageSettings.AvailableExchanges = tickers
.Exchanges
.Select(c => (c.DisplayName, c.Name))
.ToArray();
await Task.Delay(TimeSpan.FromHours(5), Cancellation);
}
async Task RefreshCoinAverageSettings()
{
await new SynchronizationContextRemover();
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
_RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes);
if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey))
{ {
try _coinAverageSettings.KeyPair = (rates.PublicKey, rates.PrivateKey);
{
await act();
}
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
{
}
catch (Exception ex)
{
Logs.PayServer.LogWarning(ex, caller + " failed");
try
{
await Task.Delay(TimeSpan.FromMinutes(1), cancellation);
}
catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { }
}
} }
} else
Task RefreshCoinAverageSupportedExchanges(CancellationToken cancellation)
{
return Timer(async () =>
{ {
await new SynchronizationContextRemover(); _coinAverageSettings.KeyPair = null;
var tickers = await new CoinAverageRateProvider("BTC").GetExchangeTickersAsync(); }
_coinAverageSettings.AvailableExchanges = tickers await _SettingsRepository.WaitSettingsChanged<RatesSetting>(Cancellation);
.Exchanges
.Select(c => (c.DisplayName, c.Name))
.ToArray();
await Task.Delay(TimeSpan.FromHours(5), cancellation);
}, cancellation);
}
Task RefreshCoinAverageSettings(CancellationToken cancellation)
{
return Timer(async () =>
{
await new SynchronizationContextRemover();
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
_RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes);
if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey))
{
_coinAverageSettings.KeyPair = (rates.PublicKey, rates.PrivateKey);
}
await _SettingsRepository.WaitSettingsChanged<RatesSetting>(cancellation);
}, cancellation);
}
public Task StopAsync(CancellationToken cancellationToken)
{
_Cts.Cancel();
return Task.WhenAll(_Tasks.ToArray());
} }
} }
} }

View File

@ -38,55 +38,13 @@ using Microsoft.Extensions.Caching.Memory;
using BTCPayServer.Logging; using BTCPayServer.Logging;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using Meziantou.AspNetCore.BundleTagHelpers; using Meziantou.AspNetCore.BundleTagHelpers;
using System.Security.Claims;
using BTCPayServer.Security;
namespace BTCPayServer.Hosting namespace BTCPayServer.Hosting
{ {
public static class BTCPayServerServices public static class BTCPayServerServices
{ {
public class OwnStoreAuthorizationRequirement : IAuthorizationRequirement
{
public OwnStoreAuthorizationRequirement()
{
}
public OwnStoreAuthorizationRequirement(string role)
{
Role = role;
}
public string Role
{
get; set;
}
}
public class OwnStoreHandler : AuthorizationHandler<OwnStoreAuthorizationRequirement>
{
StoreRepository _StoreRepository;
UserManager<ApplicationUser> _UserManager;
public OwnStoreHandler(StoreRepository storeRepository, UserManager<ApplicationUser> userManager)
{
_StoreRepository = storeRepository;
_UserManager = userManager;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OwnStoreAuthorizationRequirement requirement)
{
object storeId = null;
if (!((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).RouteData.Values.TryGetValue("storeId", out storeId))
context.Succeed(requirement);
else if (storeId != null)
{
var user = _UserManager.GetUserId(((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).HttpContext.User);
if (user != null)
{
var store = await _StoreRepository.FindStore((string)storeId, user);
if (store != null)
if (requirement.Role == null || requirement.Role == store.Role)
context.Succeed(requirement);
}
}
}
}
public static IServiceCollection AddBTCPayServer(this IServiceCollection services) public static IServiceCollection AddBTCPayServer(this IServiceCollection services)
{ {
services.AddDbContext<ApplicationDbContext>((provider, o) => services.AddDbContext<ApplicationDbContext>((provider, o) =>
@ -110,7 +68,7 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<TokenRepository>(); services.TryAddSingleton<TokenRepository>();
services.TryAddSingleton<EventAggregator>(); services.TryAddSingleton<EventAggregator>();
services.TryAddSingleton<CoinAverageSettings>(); services.TryAddSingleton<CoinAverageSettings>();
services.TryAddSingleton<ICoinAverageAuthenticator, CoinAverageSettings>(); services.TryAddSingleton<ICoinAverageAuthenticator, CoinAverageSettingsAuthenticator>();
services.TryAddSingleton<ApplicationDbContextFactory>(o => services.TryAddSingleton<ApplicationDbContextFactory>(o =>
{ {
var opts = o.GetRequiredService<BTCPayServerOptions>(); var opts = o.GetRequiredService<BTCPayServerOptions>();
@ -138,7 +96,6 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<LanguageService>(); services.TryAddSingleton<LanguageService>();
services.TryAddSingleton<NBXplorerDashboard>(); services.TryAddSingleton<NBXplorerDashboard>();
services.TryAddSingleton<CssThemeManager>();
services.TryAddSingleton<StoreRepository>(); services.TryAddSingleton<StoreRepository>();
services.TryAddSingleton<BTCPayWalletProvider>(); services.TryAddSingleton<BTCPayWalletProvider>();
services.TryAddSingleton<CurrencyNameTable>(); services.TryAddSingleton<CurrencyNameTable>();
@ -148,6 +105,9 @@ namespace BTCPayServer.Hosting
BlockTarget = 20 BlockTarget = 20
}); });
services.AddSingleton<CssThemeManager>();
services.AddSingleton<IHostedService, CssThemeManagerHostedService>();
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>(); services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>(); services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
@ -158,6 +118,8 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, InvoiceNotificationManager>(); services.AddSingleton<IHostedService, InvoiceNotificationManager>();
services.AddSingleton<IHostedService, InvoiceWatcher>(); services.AddSingleton<IHostedService, InvoiceWatcher>();
services.AddSingleton<IHostedService, RatesHostedService>(); services.AddSingleton<IHostedService, RatesHostedService>();
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
services.AddTransient<IConfigureOptions<MvcOptions>, BitpayClaimsFilter>();
services.TryAddSingleton<ExplorerClientProvider>(); services.TryAddSingleton<ExplorerClientProvider>();
services.TryAddSingleton<Bitpay>(o => services.TryAddSingleton<Bitpay>(o =>
@ -170,27 +132,14 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<IRateProviderFactory, BTCPayRateProviderFactory>(); services.TryAddSingleton<IRateProviderFactory, BTCPayRateProviderFactory>();
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>(); services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
services.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>();
services.AddTransient<AccessTokenController>(); services.AddTransient<AccessTokenController>();
services.AddTransient<InvoiceController>(); services.AddTransient<InvoiceController>();
// Add application services. // Add application services.
services.AddTransient<IEmailSender, EmailSender>(); services.AddTransient<IEmailSender, EmailSender>();
services.AddAuthorization(o =>
{
o.AddPolicy(StorePolicies.CanAccessStores, builder =>
{
builder.AddRequirements(new OwnStoreAuthorizationRequirement());
});
o.AddPolicy(StorePolicies.OwnStore, builder =>
{
builder.AddRequirements(new OwnStoreAuthorizationRequirement(StoreRoles.Owner));
});
});
// bundling // bundling
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));
services.AddBundles(); services.AddBundles();
services.AddTransient<BundleOptions>(provider => services.AddTransient<BundleOptions>(provider =>
{ {

View File

@ -6,37 +6,25 @@ using System.Collections.Generic;
using System.Text; using System.Text;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.Crypto;
using NBitcoin.DataEncoders;
using Microsoft.AspNetCore.Http.Internal;
using System.IO; using System.IO;
using BTCPayServer.Authentication; using BTCPayServer.Authentication;
using System.Security.Principal;
using NBitpayClient.Extensions;
using BTCPayServer.Logging; using BTCPayServer.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using BTCPayServer.Models; using BTCPayServer.Models;
using BTCPayServer.Configuration; using BTCPayServer.Configuration;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Http.Extensions;
using BTCPayServer.Controllers;
using System.Net.WebSockets; using System.Net.WebSockets;
using BTCPayServer.Services.Stores;
namespace BTCPayServer.Hosting namespace BTCPayServer.Hosting
{ {
public class BTCPayMiddleware public class BTCPayMiddleware
{ {
TokenRepository _TokenRepository;
RequestDelegate _Next; RequestDelegate _Next;
BTCPayServerOptions _Options; BTCPayServerOptions _Options;
public BTCPayMiddleware(RequestDelegate next, public BTCPayMiddleware(RequestDelegate next,
TokenRepository tokenRepo,
BTCPayServerOptions options) BTCPayServerOptions options)
{ {
_TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo));
_Next = next ?? throw new ArgumentNullException(nameof(next)); _Next = next ?? throw new ArgumentNullException(nameof(next));
_Options = options ?? throw new ArgumentNullException(nameof(options)); _Options = options ?? throw new ArgumentNullException(nameof(options));
} }
@ -45,42 +33,16 @@ namespace BTCPayServer.Hosting
public async Task Invoke(HttpContext httpContext) public async Task Invoke(HttpContext httpContext)
{ {
RewriteHostIfNeeded(httpContext); RewriteHostIfNeeded(httpContext);
httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
var sig = values.FirstOrDefault();
httpContext.Request.Headers.TryGetValue("x-identity", out values);
var id = values.FirstOrDefault();
if (!string.IsNullOrEmpty(sig) && !string.IsNullOrEmpty(id))
{
httpContext.Request.EnableRewind();
string body = string.Empty;
if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null)
{
using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true))
{
body = reader.ReadToEnd();
}
httpContext.Request.Body.Position = 0;
}
var url = httpContext.Request.GetEncodedUrl();
try
{
var key = new PubKey(id);
if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
{
var bitid = new BitIdentity(key);
httpContext.User = new GenericPrincipal(bitid, Array.Empty<string>());
Logs.PayServer.LogDebug($"BitId signature check success for SIN {bitid.SIN}");
}
}
catch (FormatException) { }
if (!(httpContext.User.Identity is BitIdentity))
Logs.PayServer.LogDebug("BitId signature check failed");
}
try try
{ {
var bitpayAuth = GetBitpayAuth(httpContext, out bool isBitpayAuth);
var isBitpayAPI = IsBitpayAPI(httpContext, isBitpayAuth);
httpContext.SetIsBitpayAPI(isBitpayAPI);
if (isBitpayAPI)
{
httpContext.SetBitpayAuth(bitpayAuth);
}
await _Next(httpContext); await _Next(httpContext);
} }
catch (WebSocketException) catch (WebSocketException)
@ -100,6 +62,55 @@ namespace BTCPayServer.Hosting
} }
} }
private static (string Signature, String Id, String Authorization) GetBitpayAuth(HttpContext httpContext, out bool hasBitpayAuth)
{
httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
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();
hasBitpayAuth = auth != null || (sig != null && id != null);
return (sig, id, auth);
}
private bool IsBitpayAPI(HttpContext httpContext, bool bitpayAuth)
{
if (!httpContext.Request.Path.HasValue)
return false;
var path = httpContext.Request.Path.Value;
if (
bitpayAuth &&
path == "/invoices" &&
httpContext.Request.Method == "POST" &&
(httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
return true;
if (
bitpayAuth &&
path == "/invoices" &&
httpContext.Request.Method == "GET")
return true;
if (
bitpayAuth &&
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET")
return true;
if (path.Equals("/rates", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET")
return true;
if (
path.Equals("/tokens", StringComparison.Ordinal) &&
( httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
return true;
return false;
}
private void RewriteHostIfNeeded(HttpContext httpContext) private void RewriteHostIfNeeded(HttpContext httpContext)
{ {
string reverseProxyScheme = null; string reverseProxyScheme = null;
@ -132,7 +143,7 @@ namespace BTCPayServer.Hosting
httpContext.Request.Scheme = reverseProxyScheme; httpContext.Request.Scheme = reverseProxyScheme;
} }
else else
{ {
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme; httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
} }
if (_Options.ExternalUrl.IsDefaultPort) if (_Options.ExternalUrl.IsDefaultPort)

View File

@ -0,0 +1,553 @@
// <auto-generated />
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<string>("Address")
.ValueGeneratedOnAdd();
b.Property<DateTimeOffset?>("CreatedTime");
b.Property<string>("InvoiceDataId");
b.HasKey("Address");
b.HasIndex("InvoiceDataId");
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasMaxLength(50);
b.Property<string>("StoreId")
.HasMaxLength(50);
b.HasKey("Id");
b.HasIndex("StoreId");
b.ToTable("ApiKeys");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("AppType");
b.Property<DateTimeOffset>("Created");
b.Property<string>("Name");
b.Property<string>("Settings");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Apps");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("Address");
b.Property<DateTimeOffset>("Assigned");
b.Property<string>("CryptoCode");
b.Property<DateTimeOffset?>("UnAssigned");
b.HasKey("InvoiceDataId", "Address");
b.ToTable("HistoricalAddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<DateTimeOffset>("Created");
b.Property<string>("CustomerEmail");
b.Property<string>("ExceptionStatus");
b.Property<string>("ItemCode");
b.Property<string>("OrderId");
b.Property<string>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Invoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("UniqueId");
b.Property<string>("Message");
b.Property<DateTimeOffset>("Timestamp");
b.HasKey("InvoiceDataId", "UniqueId");
b.ToTable("InvoiceEvents");
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<DateTimeOffset>("PairingTime");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("SIN");
b.HasIndex("StoreDataId");
b.ToTable("PairedSINData");
});
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("DateCreated");
b.Property<DateTimeOffset>("Expiration");
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.Property<string>("TokenValue");
b.HasKey("Id");
b.ToTable("PairingCodes");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Accounted");
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("Payments");
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.HasKey("Id");
b.ToTable("PendingInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("RefundAddresses");
});
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("DefaultCrypto");
b.Property<string>("DerivationStrategies");
b.Property<string>("DerivationStrategy");
b.Property<int>("SpeedPolicy");
b.Property<byte[]>("StoreBlob");
b.Property<byte[]>("StoreCertificate");
b.Property<string>("StoreName");
b.Property<string>("StoreWebsite");
b.HasKey("Id");
b.ToTable("Stores");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.Property<string>("ApplicationUserId");
b.Property<string>("StoreDataId");
b.Property<string>("Role");
b.HasKey("ApplicationUserId", "StoreDataId");
b.HasIndex("StoreDataId");
b.ToTable("UserStore");
});
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<bool>("RequiresEmailConfirmation");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("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<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("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<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", 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<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -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<string>(maxLength: 50, nullable: false),
StoreId = table.Column<string>(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");
}
}
}

View File

@ -18,7 +18,7 @@ namespace BTCPayServer.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "2.0.1-rtm-125"); .HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{ {
@ -36,6 +36,22 @@ namespace BTCPayServer.Migrations
b.ToTable("AddressInvoices"); b.ToTable("AddressInvoices");
}); });
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasMaxLength(50);
b.Property<string>("StoreId")
.HasMaxLength(50);
b.HasKey("Id");
b.HasIndex("StoreId");
b.ToTable("ApiKeys");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b => modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")

View File

@ -17,5 +17,8 @@ namespace BTCPayServer.Models.AppViewModels
[Required] [Required]
[MaxLength(5000)] [MaxLength(5000)]
public string Template { get; set; } public string Template { get; set; }
[Display(Name = "User can input custom amount")]
public bool ShowCustomAmount { get; set; }
} }
} }

View File

@ -18,6 +18,8 @@ namespace BTCPayServer.Models.AppViewModels
public ItemPrice Price { get; set; } public ItemPrice Price { get; set; }
public string Title { get; set; } public string Title { get; set; }
} }
public bool ShowCustomAmount { get; set; }
public string Step { get; set; }
public string Title { get; set; } public string Title { get; set; }
public Item[] Items { get; set; } public Item[] Items { get; set; }
} }

View File

@ -24,6 +24,7 @@ namespace BTCPayServer.Models.InvoicingModels
public string BtcAddress { get; set; } public string BtcAddress { get; set; }
public string BtcDue { get; set; } public string BtcDue { get; set; }
public string CustomerEmail { get; set; } public string CustomerEmail { get; set; }
public bool RequiresRefundEmail { get; set; }
public int ExpirationSeconds { get; set; } public int ExpirationSeconds { get; set; }
public string Status { get; set; } public string Status { get; set; }
public string MerchantRefLink { get; set; } public string MerchantRefLink { get; set; }

View File

@ -31,6 +31,12 @@ namespace BTCPayServer.Models.StoreViewModels
[MaxLength(20)] [MaxLength(20)]
public string LightningMaxValue { get; set; } public string LightningMaxValue { get; set; }
[Display(Name = "Requires a refund email")]
public bool RequiresRefundEmail
{
get; set;
}
[Display(Name = "Do not propose on chain payment if the value of the invoice is below...")] [Display(Name = "Do not propose on chain payment if the value of the invoice is below...")]
[MaxLength(20)] [MaxLength(20)]
public string OnChainMinValue { get; set; } public string OnChainMinValue { get; set; }

View File

@ -68,5 +68,9 @@ namespace BTCPayServer.Models.StoreViewModels
get; get;
set; set;
} }
[Display(Name = "API Key")]
public string ApiKey { get; set; }
public string EncodedApiKey { get; set; }
} }
} }

View File

@ -1111,4 +1111,17 @@ namespace BTCPayServer
#endregion #endregion
} }
} }
public static class MultiValueDictionaryExtensions
{
public static MultiValueDictionary<TKey, TValue> ToMultiValueDictionary<TInput, TKey, TValue>(this IEnumerable<TInput> collection, Func<TInput, TKey> keySelector, Func<TInput, TValue> valueSelector)
{
var dictionary = new MultiValueDictionary<TKey, TValue>();
foreach(var item in collection)
{
dictionary.Add(keySelector(item), valueSelector(item));
}
return dictionary;
}
}
} }

View File

@ -21,7 +21,7 @@ namespace BTCPayServer
.Select(t => t.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries)) .Select(t => t.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries))
.Where(kv => kv.Length == 2) .Where(kv => kv.Length == 2)
.Select(kv => new KeyValuePair<string, string>(kv[0].ToLowerInvariant(), kv[1])) .Select(kv => new KeyValuePair<string, string>(kv[0].ToLowerInvariant(), kv[1]))
.ToDictionary(o => o.Key, o => o.Value); .ToMultiValueDictionary(o => o.Key, o => o.Value);
foreach(var filter in splitted) foreach(var filter in splitted)
{ {
@ -38,8 +38,8 @@ namespace BTCPayServer
get; get;
private set; private set;
} }
public Dictionary<string, string> Filters { get; private set; } public MultiValueDictionary<string, string> Filters { get; private set; }
public override string ToString() public override string ToString()
{ {

View File

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using BTCPayServer.Models;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Security
{
public class BTCPayClaimsFilter : IAsyncAuthorizationFilter, IConfigureOptions<MvcOptions>
{
UserManager<ApplicationUser> _UserManager;
StoreRepository _StoreRepository;
public BTCPayClaimsFilter(
UserManager<ApplicationUser> userManager,
StoreRepository storeRepository)
{
_UserManager = userManager;
_StoreRepository = storeRepository;
}
void IConfigureOptions<MvcOptions>.Configure(MvcOptions options)
{
options.Filters.Add(typeof(BTCPayClaimsFilter));
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var principal = context.HttpContext.User;
if (!context.HttpContext.GetIsBitpayAPI())
{
var identity = ((ClaimsIdentity)principal.Identity);
if (principal.IsInRole(Roles.ServerAdmin))
{
identity.AddClaim(new Claim(Policies.CanModifyServerSettings.Key, "true"));
}
if (context.RouteData.Values.TryGetValue("storeId", out var storeId))
{
var claim = identity.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
if (claim != null)
{
var store = await _StoreRepository.FindStore((string)storeId, claim.Value);
if (store == null)
context.Result = new ChallengeResult(Policies.CookieAuthentication);
else
{
context.HttpContext.SetStoreData(store);
if (store != null)
{
identity.AddClaims(store.GetClaims());
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,196 @@
using System;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Http.Extensions;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Authentication;
using BTCPayServer.Models;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitpayClient;
using NBitpayClient.Extensions;
using Newtonsoft.Json.Linq;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.Http.Internal;
namespace BTCPayServer.Security
{
public class BitpayClaimsFilter : IAsyncAuthorizationFilter, IConfigureOptions<MvcOptions>
{
UserManager<ApplicationUser> _UserManager;
StoreRepository _StoreRepository;
TokenRepository _TokenRepository;
public BitpayClaimsFilter(
UserManager<ApplicationUser> userManager,
TokenRepository tokenRepository,
StoreRepository storeRepository)
{
_UserManager = userManager;
_StoreRepository = storeRepository;
_TokenRepository = tokenRepository;
}
void IConfigureOptions<MvcOptions>.Configure(MvcOptions options)
{
options.Filters.Add(typeof(BitpayClaimsFilter));
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var principal = context.HttpContext.User;
if (context.HttpContext.GetIsBitpayAPI())
{
var bitpayAuth = context.HttpContext.GetBitpayAuth();
string storeId = null;
var failedAuth = false;
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
{
storeId = await CheckBitId(context.HttpContext, bitpayAuth.Signature, bitpayAuth.Id);
if (!context.HttpContext.User.Claims.Any(c => c.Type == Claims.SIN))
{
Logs.PayServer.LogDebug("BitId signature check failed");
failedAuth = true;
}
}
else if (!string.IsNullOrEmpty(bitpayAuth.Authorization))
{
storeId = await CheckLegacyAPIKey(context.HttpContext, bitpayAuth.Authorization);
if (storeId == null)
{
Logs.PayServer.LogDebug("API key check failed");
failedAuth = true;
}
}
if (storeId != null)
{
var identity = ((ClaimsIdentity)context.HttpContext.User.Identity);
identity.AddClaim(new Claim(Claims.OwnStore, storeId));
var store = await _StoreRepository.FindStore(storeId);
context.HttpContext.SetStoreData(store);
}
else if (failedAuth)
{
throw new BitpayHttpException(401, "Can't access to store");
}
}
}
private async Task<string> CheckBitId(HttpContext httpContext, string sig, string id)
{
httpContext.Request.EnableRewind();
string storeId = null;
string body = string.Empty;
if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null)
{
using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true))
{
body = reader.ReadToEnd();
}
httpContext.Request.Body.Position = 0;
}
var url = httpContext.Request.GetEncodedUrl();
try
{
var key = new PubKey(id);
if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
{
var sin = key.GetBitIDSIN();
var identity = ((ClaimsIdentity)httpContext.User.Identity);
identity.AddClaim(new Claim(Claims.SIN, sin));
string token = null;
if (httpContext.Request.Query.TryGetValue("token", out var tokenValues))
{
token = tokenValues[0];
}
if (token == null && !String.IsNullOrEmpty(body) && httpContext.Request.Method == "POST")
{
try
{
token = JObject.Parse(body)?.Property("token")?.Value?.Value<string>();
}
catch { }
}
if (token != null)
{
var bitToken = await GetTokenPermissionAsync(sin, token);
if (bitToken == null)
{
return null;
}
storeId = bitToken.StoreId;
}
}
}
catch (FormatException) { }
return storeId;
}
private async Task<string> CheckLegacyAPIKey(HttpContext httpContext, string auth)
{
var splitted = auth.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase))
{
return null;
}
string apiKey = null;
try
{
apiKey = Encoders.ASCII.EncodeData(Encoders.Base64.DecodeData(splitted[1]));
}
catch
{
return null;
}
return await _TokenRepository.GetStoreIdFromAPIKey(apiKey);
}
private async Task<BitTokenEntity> GetTokenPermissionAsync(string sin, string expectedToken)
{
var actualTokens = (await _TokenRepository.GetTokens(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.Merchant} for SIN {sin}");
return null;
}
return actualToken;
}
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
{
if (token.Facade == Facade.Merchant.ToString())
{
yield return token.Clone(Facade.User);
yield return token.Clone(Facade.PointOfSale);
}
if (token.Facade == Facade.PointOfSale.ToString())
{
yield return token.Clone(Facade.User);
}
yield return token;
}
}
}

View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
namespace BTCPayServer.Security
{
public static class Policies
{
public const string CookieAuthentication = "Identity.Application";
public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options)
{
AddClaim(options, CanUseStore.Key);
AddClaim(options, CanModifyStoreSettings.Key);
AddClaim(options, CanModifyServerSettings.Key);
return options;
}
private static void AddClaim(AuthorizationOptions options, string key)
{
options.AddPolicy(key, o => o.RequireClaim(key));
}
public class CanModifyServerSettings
{
public const string Key = "btcpay.store.canmodifyserversettings";
}
public class CanUseStore
{
public const string Key = "btcpay.store.canusestore";
}
public class CanModifyStoreSettings
{
public const string Key = "btcpay.store.canmodifystoresettings";
}
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services
{
public class Claims
{
public const string SIN = "BITID_SIN";
public const string OwnStore = "BTCPAY_OWN_STORE";
}
}

View File

@ -99,10 +99,14 @@ namespace BTCPayServer.Services
try try
{ {
var pubKey = await ledger.GetWalletPubKeyAsync(account); var pubKey = await ledger.GetWalletPubKeyAsync(account);
if (pubKey.Address.Network != network.NBitcoinNetwork) try
{
pubKey.GetAddress(network.NBitcoinNetwork);
}
catch
{ {
if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet) if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet)
throw new Exception($"The opened ledger app should be for {network.NBitcoinNetwork.Name}, not for {pubKey.Address.Network}"); 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)).UncompressedPublicKey.Compress().Hash.ToBytes().Take(4).ToArray(); var fingerprint = onlyChaincode ? new byte[4] : (await ledger.GetWalletPubKeyAsync(account.Parent)).UncompressedPublicKey.Compress().Hash.ToBytes().Take(4).ToArray();
var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(), pubKey.ChainCode, (byte)account.Indexes.Length, fingerprint, account.Indexes.Last()).GetWif(network.NBitcoinNetwork); var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(), pubKey.ChainCode, (byte)account.Indexes.Length, fingerprint, account.Indexes.Last()).GetWif(network.NBitcoinNetwork);
@ -195,6 +199,7 @@ namespace BTCPayServer.Services
TransactionBuilder builder = new TransactionBuilder(); TransactionBuilder builder = new TransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = minTxRelayFee; builder.StandardTransactionPolicy.MinRelayTxFee = minTxRelayFee;
builder.SetConsensusFactory(network.NBitcoinNetwork);
builder.AddCoins(coins.Select(c=>c.Coin).ToArray()); builder.AddCoins(coins.Select(c=>c.Coin).ToArray());
foreach (var element in send) foreach (var element in send)

View File

@ -399,9 +399,10 @@ namespace BTCPayServer.Services.Invoices
query = query.Where(i => i.Id == queryObject.InvoiceId); query = query.Where(i => i.Id == queryObject.InvoiceId);
} }
if (!string.IsNullOrEmpty(queryObject.StoreId)) if (queryObject.StoreId != null && queryObject.StoreId.Length > 0)
{ {
query = query.Where(i => i.StoreDataId == queryObject.StoreId); var stores = queryObject.StoreId.ToHashSet();
query = query.Where(i => stores.Contains(i.StoreDataId));
} }
if (queryObject.UserId != null) if (queryObject.UserId != null)
@ -429,8 +430,11 @@ namespace BTCPayServer.Services.Invoices
if (queryObject.OrderId != null) if (queryObject.OrderId != null)
query = query.Where(i => i.OrderId == queryObject.OrderId); query = query.Where(i => i.OrderId == queryObject.OrderId);
if (queryObject.Status != null) if (queryObject.Status != null && queryObject.Status.Length > 0)
query = query.Where(i => i.Status == queryObject.Status); {
var statusSet = queryObject.Status.ToHashSet();
query = query.Where(i => statusSet.Contains(i.Status));
}
query = query.OrderByDescending(q => q.Created); query = query.OrderByDescending(q => q.Created);
@ -568,7 +572,7 @@ namespace BTCPayServer.Services.Invoices
public class InvoiceQuery public class InvoiceQuery
{ {
public string StoreId public string[] StoreId
{ {
get; set; get; set;
} }
@ -610,7 +614,7 @@ namespace BTCPayServer.Services.Invoices
get; set; get; set;
} }
public string Status public string[] Status
{ {
get; set; get; set;
} }

View File

@ -31,6 +31,7 @@ namespace BTCPayServer.Services
new Language("nl-NL", "Dutch"), new Language("nl-NL", "Dutch"),
new Language("cs-CZ", "Česky"), new Language("cs-CZ", "Česky"),
new Language("is-IS", "Íslenska"), new Language("is-IS", "Íslenska"),
new Language("hr-HR", "Croatian"),
}; };
} }
} }

View File

@ -65,9 +65,17 @@ namespace BTCPayServer.Services.Rates
private IRateProvider CreateExchangeRateProvider(BTCPayNetwork network, string exchange) private IRateProvider CreateExchangeRateProvider(BTCPayNetwork network, string exchange)
{ {
List<IRateProvider> providers = new List<IRateProvider>();
if(exchange == "quadrigacx")
{
providers.Add(new QuadrigacxRateProvider(network.CryptoCode));
}
var coinAverage = new CoinAverageRateProviderDescription(network.CryptoCode).CreateRateProvider(serviceProvider); var coinAverage = new CoinAverageRateProviderDescription(network.CryptoCode).CreateRateProvider(serviceProvider);
coinAverage.Exchange = exchange; coinAverage.Exchange = exchange;
return coinAverage; providers.Add(coinAverage);
return new FallbackRateProvider(providers.ToArray());
} }
private CachedRateProvider CreateCachedRateProvider(BTCPayNetwork network, IRateProvider rateProvider, string additionalScope) private CachedRateProvider CreateCachedRateProvider(BTCPayNetwork network, IRateProvider rateProvider, string additionalScope)

View File

@ -204,6 +204,11 @@ namespace BTCPayServer.Services.Rates
public async Task<GetExchangeTickersResponse> GetExchangeTickersAsync() public async Task<GetExchangeTickersResponse> GetExchangeTickersAsync()
{ {
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/symbols/exchanges/ticker"); var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/symbols/exchanges/ticker");
var auth = Authenticator;
if (auth != null)
{
await auth.AddHeader(request);
}
var resp = await _Client.SendAsync(request); var resp = await _Client.SendAsync(request);
resp.EnsureSuccessStatusCode(); resp.EnsureSuccessStatusCode();
var jobj = JObject.Parse(await resp.Content.ReadAsStringAsync()); var jobj = JObject.Parse(await resp.Content.ReadAsStringAsync());

View File

@ -8,6 +8,18 @@ using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates namespace BTCPayServer.Services.Rates
{ {
public class CoinAverageSettingsAuthenticator : ICoinAverageAuthenticator
{
CoinAverageSettings _Settings;
public CoinAverageSettingsAuthenticator(CoinAverageSettings settings)
{
_Settings = settings;
}
public Task AddHeader(HttpRequestMessage message)
{
return _Settings.AddHeader(message);
}
}
public class CoinAverageSettings : ICoinAverageAuthenticator public class CoinAverageSettings : ICoinAverageAuthenticator
{ {
private static readonly DateTime _epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); private static readonly DateTime _epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
@ -15,6 +27,71 @@ namespace BTCPayServer.Services.Rates
public (String PublicKey, String PrivateKey)? KeyPair { get; set; } public (String PublicKey, String PrivateKey)? KeyPair { get; set; }
public (String DisplayName, String Name)[] AvailableExchanges { get; set; } = Array.Empty<(String DisplayName, String Name)>(); public (String DisplayName, String Name)[] AvailableExchanges { get; set; } = Array.Empty<(String DisplayName, String Name)>();
public CoinAverageSettings()
{
//GENERATED BY:
//StringBuilder b = new StringBuilder();
//b.AppendLine("_coinAverageSettings.AvailableExchanges = new[] {");
//foreach (var availableExchange in _coinAverageSettings.AvailableExchanges)
//{
// b.AppendLine($"(DisplayName: \"{availableExchange.DisplayName}\", Name: \"{availableExchange.Name}\"),");
//}
//b.AppendLine("}.ToArray()");
AvailableExchanges = new[] {
(DisplayName: "BitBargain", Name: "bitbargain"),
(DisplayName: "Tidex", Name: "tidex"),
(DisplayName: "LocalBitcoins", Name: "localbitcoins"),
(DisplayName: "EtherDelta", Name: "etherdelta"),
(DisplayName: "Kraken", Name: "kraken"),
(DisplayName: "BitBay", Name: "bitbay"),
(DisplayName: "Independent Reserve", Name: "independentreserve"),
(DisplayName: "Exmoney", Name: "exmoney"),
(DisplayName: "Bitcoin.co.id", Name: "bitcoin_co_id"),
(DisplayName: "Huobi", Name: "huobi"),
(DisplayName: "GDAX", Name: "gdax"),
(DisplayName: "Coincheck", Name: "coincheck"),
(DisplayName: "Bittylicious", Name: "bittylicious"),
(DisplayName: "Gemini", Name: "gemini"),
(DisplayName: "QuadrigaCX", Name: "quadrigacx"),
(DisplayName: "Bit2C", Name: "bit2c"),
(DisplayName: "Luno", Name: "luno"),
(DisplayName: "Negocie Coins", Name: "negociecoins"),
(DisplayName: "FYB-SE", Name: "fybse"),
(DisplayName: "Hitbtc", Name: "hitbtc"),
(DisplayName: "Bitex.la", Name: "bitex"),
(DisplayName: "Korbit", Name: "korbit"),
(DisplayName: "itBit", Name: "itbit"),
(DisplayName: "Okex", Name: "okex"),
(DisplayName: "Bitsquare", Name: "bitsquare"),
(DisplayName: "Bitfinex", Name: "bitfinex"),
(DisplayName: "CoinMate", Name: "coinmate"),
(DisplayName: "Bitstamp", Name: "bitstamp"),
(DisplayName: "Cryptonit", Name: "cryptonit"),
(DisplayName: "Foxbit", Name: "foxbit"),
(DisplayName: "QuickBitcoin", Name: "quickbitcoin"),
(DisplayName: "Poloniex", Name: "poloniex"),
(DisplayName: "Bit-Z", Name: "bitz"),
(DisplayName: "Liqui", Name: "liqui"),
(DisplayName: "BitKonan", Name: "bitkonan"),
(DisplayName: "Kucoin", Name: "kucoin"),
(DisplayName: "Binance", Name: "binance"),
(DisplayName: "Rock Trading", Name: "rocktrading"),
(DisplayName: "Mercado Bitcoin", Name: "mercado"),
(DisplayName: "Coinsecure", Name: "coinsecure"),
(DisplayName: "Coinfloor", Name: "coinfloor"),
(DisplayName: "bitFlyer", Name: "bitflyer"),
(DisplayName: "BTCTurk", Name: "btcturk"),
(DisplayName: "Bittrex", Name: "bittrex"),
(DisplayName: "CampBX", Name: "campbx"),
(DisplayName: "Zaif", Name: "zaif"),
(DisplayName: "FYB-SG", Name: "fybsg"),
(DisplayName: "Quoine", Name: "quoine"),
(DisplayName: "BTC Markets", Name: "btcmarkets"),
(DisplayName: "Bitso", Name: "bitso"),
}.ToArray();
}
public Task AddHeader(HttpRequestMessage message) public Task AddHeader(HttpRequestMessage message)
{ {
var signature = GetCoinAverageSignature(); var signature = GetCoinAverageSignature();

View File

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class QuadrigacxRateProvider : IRateProvider
{
public QuadrigacxRateProvider(string crypto)
{
CryptoCode = crypto;
}
public string CryptoCode { get; set; }
static HttpClient _Client = new HttpClient();
public async Task<decimal> GetRateAsync(string currency)
{
return await GetRatesAsyncCore(CryptoCode, currency);
}
private async Task<decimal> GetRatesAsyncCore(string cryptoCode, string currency)
{
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book={cryptoCode.ToLowerInvariant()}_{currency.ToLowerInvariant()}");
response.EnsureSuccessStatusCode();
var rates = JObject.Parse(await response.Content.ReadAsStringAsync());
if (!TryToDecimal(rates, out var result))
throw new RateUnavailableException(currency);
return result;
}
private bool TryToDecimal(JObject p, out decimal v)
{
v = 0.0m;
JToken token = p.Property("bid")?.Value;
if (token == null)
return false;
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
}
public async Task<ICollection<Rate>> GetRatesAsync()
{
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book=all");
response.EnsureSuccessStatusCode();
var rates = JObject.Parse(await response.Content.ReadAsStringAsync());
List<Rate> result = new List<Rate>();
foreach (var prop in rates.Properties())
{
var rate = new Rate();
var splitted = prop.Name.Split('_');
var crypto = splitted[0].ToUpperInvariant();
if (crypto != CryptoCode)
continue;
rate.Currency = splitted[1].ToUpperInvariant();
TryToDecimal((JObject)prop.Value, out var v);
rate.Value = v;
result.Add(rate);
}
return result;
}
}
}

View File

@ -20,6 +20,8 @@ namespace BTCPayServer.Services.Stores
public async Task<StoreData> FindStore(string storeId) public async Task<StoreData> FindStore(string storeId)
{ {
if (storeId == null)
return null;
using (var ctx = _ContextFactory.CreateContext()) using (var ctx = _ContextFactory.CreateContext())
{ {
return await ctx.FindAsync<StoreData>(storeId).ConfigureAwait(false); return await ctx.FindAsync<StoreData>(storeId).ConfigureAwait(false);
@ -42,7 +44,9 @@ namespace BTCPayServer.Services.Stores
}).ToArrayAsync()) }).ToArrayAsync())
.Select(us => .Select(us =>
{ {
#pragma warning disable CS0612 // Type or member is obsolete
us.Store.Role = us.Role; us.Store.Role = us.Role;
#pragma warning restore CS0612 // Type or member is obsolete
return us.Store; return us.Store;
}).FirstOrDefault(); }).FirstOrDefault();
} }
@ -82,7 +86,9 @@ namespace BTCPayServer.Services.Stores
.ToArrayAsync()) .ToArrayAsync())
.Select(u => .Select(u =>
{ {
#pragma warning disable CS0612 // Type or member is obsolete
u.StoreData.Role = u.Role; u.StoreData.Role = u.Role;
#pragma warning restore CS0612 // Type or member is obsolete
return u.StoreData; return u.StoreData;
}).ToArray(); }).ToArray();
} }

View File

@ -5,11 +5,6 @@ using System.Threading.Tasks;
namespace BTCPayServer namespace BTCPayServer
{ {
public class StorePolicies
{
public const string CanAccessStores = "CanAccessStore";
public const string OwnStore = "OwnStore";
}
public class StoreRoles public class StoreRoles
{ {
public const string Owner = "Owner"; public const string Owner = "Owner";

View File

@ -29,6 +29,10 @@
<input asp-for="Currency" class="form-control" /> <input asp-for="Currency" class="form-control" />
<span asp-validation-for="Currency" class="text-danger"></span> <span asp-validation-for="Currency" class="text-danger"></span>
</div> </div>
<div class="form-group">
<label asp-for="ShowCustomAmount"></label>
<input asp-for="ShowCustomAmount" type="checkbox" class="form-check" />
</div>
<div class="form-group"> <div class="form-group">
<label asp-for="Template" class="control-label"></label>* <label asp-for="Template" class="control-label"></label>*
<textarea asp-for="Template" rows="20" cols="40" class="form-control"></textarea> <textarea asp-for="Template" rows="20" cols="40" class="form-control"></textarea>

View File

@ -19,27 +19,31 @@
<h1 class="mb-4">@Model.Title</h1> <h1 class="mb-4">@Model.Title</h1>
<form method="post"> <form method="post">
<div class="row"> <div class="row">
@foreach (var item in Model.Items) @for(int i = 0; i < Model.Items.Length; i++)
{ {
<div class="col-sm-4 mb-3"> var className = (Model.Items.Length - i) > (Model.Items.Length % 3) ? "col-sm-4 mb-3" : "col align-self-center";
var item = Model.Items[i];
<div class="@className">
<h3>@item.Title</h3> <h3>@item.Title</h3>
<button type="submit" name="choiceKey" class="btn btn-primary" value="@item.Id">Buy for @item.Price.Formatted</button> <button type="submit" name="choiceKey" class="btn btn-primary" value="@item.Id">Buy for @item.Price.Formatted</button>
</div> </div>
} }
</div> </div>
</form> </form>
@*<div class="row mt-4"> @if(Model.ShowCustomAmount)
{
<div class="row mt-4">
<div class="col-md-4 offset-md-4 col-sm-6 offset-sm-3"> <div class="col-md-4 offset-md-4 col-sm-6 offset-sm-3">
<h3>Something else</h3> <form method="post" data-buy>
<form data-buy>
<div class="input-group"> <div class="input-group">
<input class="form-control" type="number" step="0.00001" name="amount" placeholder="undefined (optional)"><div class="input-group-append"> <input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="amount"><div class="input-group-append">
<button class="btn btn-primary" type="submit">Pay</button> <button class="btn btn-primary" type="submit">Pay</button>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
</div>*@ </div>
}
</div> </div>
</div> </div>
<script src="~/vendor/jquery/jquery.js"></script> <script src="~/vendor/jquery/jquery.js"></script>

View File

@ -114,7 +114,8 @@
'pt-BR': { translation: locales_pt_br }, 'pt-BR': { translation: locales_pt_br },
'nl': { translation: locales_nl }, 'nl': { translation: locales_nl },
'cs-CZ': { translation: locales_cs }, 'cs-CZ': { translation: locales_cs },
'is-IS': { translation: locales_is } 'is-IS': { translation: locales_is },
'hr-HR': { translation: locales_hr }
}, },
}); });

View File

@ -7,6 +7,19 @@
.linethrough { .linethrough {
text-decoration: line-through; text-decoration: line-through;
} }
.smMaxWidth {
max-width: 200px;
}
@@media (min-width: 768px) {
.smMaxWidth {
max-width: 400px;
}
}
.firstCol {
width: 140px;
}
</style> </style>
<section> <section>
@ -170,12 +183,12 @@
</table> </table>
</div> </div>
</div> </div>
@if(Model.OnChainPayments.Count > 0) @if (Model.OnChainPayments.Count > 0)
{ {
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<h3>On-Chain payments</h3> <h3>On-Chain payments</h3>
<table class="table table-sm table-responsive-md"> <table class="table table-sm table-responsive-lg">
<thead class="thead-inverse"> <thead class="thead-inverse">
<tr> <tr>
<th>Crypto</th> <th>Crypto</th>
@ -185,13 +198,17 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach(var payment in Model.OnChainPayments) @foreach (var payment in Model.OnChainPayments)
{ {
var replaced = payment.Replaced ? "class='linethrough'" : ""; var replaced = payment.Replaced ? "class='linethrough'" : "";
<tr> <tr @replaced>
<td @replaced>@payment.Crypto</td> <td>@payment.Crypto</td>
<td @replaced>@payment.DepositAddress</td> <td>@payment.DepositAddress</td>
<td @replaced><a href="@payment.TransactionLink" target="_blank">@payment.TransactionId</a></td> <td class="smMaxWidth text-truncate">
<a href="@payment.TransactionLink" target="_blank">
@payment.TransactionId
</a>
</td>
<td class="text-right">@payment.Confirmations</td> <td class="text-right">@payment.Confirmations</td>
</tr> </tr>
} }
@ -200,7 +217,7 @@
</div> </div>
</div> </div>
} }
@if(Model.OffChainPayments.Count > 0) @if (Model.OffChainPayments.Count > 0)
{ {
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
@ -208,16 +225,16 @@
<table class="table table-sm table-responsive-md"> <table class="table table-sm table-responsive-md">
<thead class="thead-inverse"> <thead class="thead-inverse">
<tr> <tr>
<th>Crypto</th> <th class="firstCol">Crypto</th>
<th>BOLT11</th> <th>BOLT11</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach(var payment in Model.OffChainPayments) @foreach (var payment in Model.OffChainPayments)
{ {
<tr> <tr>
<td>@payment.Crypto</td> <td>@payment.Crypto</td>
<td>@payment.BOLT11</td> <td class="smMaxWidth text-truncate">@payment.BOLT11</td>
</tr> </tr>
} }
</tbody> </tbody>
@ -231,17 +248,17 @@
<table class="table table-sm table-responsive-md"> <table class="table table-sm table-responsive-md">
<thead class="thead-inverse"> <thead class="thead-inverse">
<tr> <tr>
<th>Payment method</th> <th class="firstCol">Payment method</th>
<th>Address</th> <th>Address</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (var address in Model.Addresses) @foreach (var address in Model.Addresses)
{ {
var current = address.Current ? "class='font-weight-bold'" : ""; var current = address.Current ? "font-weight-bold" : "";
<tr> <tr>
<td>@address.PaymentMethod</td> <td>@address.PaymentMethod</td>
<td @current>@address.Destination</td> <td class="smMaxWidth text-truncate @current">@address.Destination</td>
</tr> </tr>
} }
</tbody> </tbody>

View File

@ -26,6 +26,9 @@
<li><b>storeid:id</b> for filtering a specific store</li> <li><b>storeid:id</b> for filtering a specific store</li>
<li><b>status:(expired|invalid|complete|confirmed|paid|new)</b> for filtering a specific status</li> <li><b>status:(expired|invalid|complete|confirmed|paid|new)</b> for filtering a specific status</li>
</ul> </ul>
<p>
If you want two confirmed and complete invoices, duplicate the filter: `status:confirmed status:complete`.
</p>
</div> </div>
<div class="form-group"> <div class="form-group">
<form asp-action="SearchInvoice" method="post"> <form asp-action="SearchInvoice" method="post">

View File

@ -4,6 +4,7 @@
@inject BTCPayServer.Services.BTCPayServerEnvironment env @inject BTCPayServer.Services.BTCPayServerEnvironment env
@inject BTCPayServer.HostedServices.NBXplorerDashboard dashboard @inject BTCPayServer.HostedServices.NBXplorerDashboard dashboard
@inject BTCPayServer.HostedServices.CssThemeManager themeManager @inject BTCPayServer.HostedServices.CssThemeManager themeManager
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers @addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
<!DOCTYPE html> <!DOCTYPE html>
@ -18,8 +19,8 @@
<title>BTCPay Server</title> <title>BTCPay Server</title>
@* CSS *@ @* CSS *@
<link href="@themeManager.BootstrapUri" rel="stylesheet" /> <link href="@this.Context.Request.GetAbsoluteUri(themeManager.BootstrapUri)" rel="stylesheet" />
<link href="@themeManager.CreativeStartUri" rel="stylesheet" /> <link href="@this.Context.Request.GetAbsoluteUri(themeManager.CreativeStartUri)" rel="stylesheet" />
<bundle name="wwwroot/bundles/main-bundle.min.css" /> <bundle name="wwwroot/bundles/main-bundle.min.css" />

View File

@ -38,6 +38,10 @@
<label asp-for="AllowCoinConversion"></label> <label asp-for="AllowCoinConversion"></label>
<input asp-for="AllowCoinConversion" type="checkbox" class="form-check" /> <input asp-for="AllowCoinConversion" type="checkbox" class="form-check" />
</div> </div>
<div class="form-group">
<label asp-for="RequiresRefundEmail"></label>
<input asp-for="RequiresRefundEmail" type="checkbox" class="form-check" />
</div>
<div class="form-group"> <div class="form-group">
<label asp-for="LightningMaxValue"></label> <label asp-for="LightningMaxValue"></label>
<input asp-for="LightningMaxValue" class="form-control" /> <input asp-for="LightningMaxValue" class="form-control" />

View File

@ -5,32 +5,60 @@
ViewData.AddActivePage(StoreNavPages.Tokens); ViewData.AddActivePage(StoreNavPages.Tokens);
} }
<h4>@ViewData["Title"]</h4>
<p>You can allow a public key to access the API of this store</p>
@Html.Partial("_StatusMessage", Model.StatusMessage) @Html.Partial("_StatusMessage", Model.StatusMessage)
<a asp-action="CreateToken" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new token</a> <h4>Access token</h4>
<table class="table table-sm table-responsive-md"> <div class="row">
<thead> <div class="col-md-8">
<tr> <p>Authorize a public key to access Bitpay compatible Invoice API (<a href="https://support.bitpay.com/hc/en-us/articles/115003001183-How-do-I-pair-my-client-and-create-a-token-">More information</a>)</p>
<th>Label</th> </div>
<th>SIN</th> </div>
<th>Facade</th> <div class="row">
<th>Actions</th> <div class="col-md-8">
</tr> <a asp-action="CreateToken" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new token</a>
</thead> <table class="table table-sm table-responsive-md">
<tbody> <thead>
@foreach (var token in Model.Tokens) <tr>
{ <th>Label</th>
<tr> <th>SIN</th>
<td>@token.Label</td> <th>Facade</th>
<td>@token.SIN</td> <th>Actions</th>
<td>@token.Facade</td> </tr>
<td> </thead>
<form asp-action="DeleteToken" method="post"> <tbody>
<input type="hidden" name="tokenId" value="@token.Id"> @foreach(var token in Model.Tokens)
<button type="submit" class="btn btn-danger" role="button">Revoke</button> {
</form> <tr>
</td> <td>@token.Label</td>
</tr>} <td>@token.SIN</td>
</tbody> <td>@token.Facade</td>
</table> <td>
<form asp-action="DeleteToken" method="post">
<input type="hidden" name="tokenId" value="@token.Id">
<button type="submit" class="btn btn-danger" role="button">Revoke</button>
</form>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<h4>Legacy API Keys</h4>
<div class="row">
<div class="col-md-8">
<p>Alternatively, you can use the invoice API by including the following HTTP Header in your requests:<br /> <code>Authorization: Basic @Model.EncodedApiKey</code> </p>
</div>
</div>
<div class="row">
<div class="col-md-8">
<form method="post" asp-action="GenerateAPIKey">
<div class="form-group">
<label asp-for="ApiKey"></label>
<input asp-for="ApiKey" readonly class="form-control" />
</div>
<button type="submit" class="btn btn-primary" role="button">Create new API Key</button>
</form>
</div>
</div>

View File

@ -18,8 +18,7 @@
"wwwroot/vendor/jquery-easing/jquery.easing.js", "wwwroot/vendor/jquery-easing/jquery.easing.js",
"wwwroot/vendor/scrollreveal/scrollreveal.min.js", "wwwroot/vendor/scrollreveal/scrollreveal.min.js",
"wwwroot/vendor/magnific-popup/jquery.magnific-popup.js", "wwwroot/vendor/magnific-popup/jquery.magnific-popup.js",
"wwwroot/vendor/bootstrap4-creativestart/*.js", "wwwroot/vendor/bootstrap4-creativestart/*.js"
"wwwroot/main/**/js/*.js"
] ]
}, },
{ {

View File

@ -104,14 +104,11 @@ $(document).ready(function () {
*/ */
var display = $(".timer-row__time-left"); // Timer container
// check if the Document expired // check if the Document expired
if (srvModel.expirationSeconds > 0) { if (srvModel.expirationSeconds > 0) {
progressStart(srvModel.maxTimeSeconds); // Progress bar progressStart(srvModel.maxTimeSeconds); // Progress bar
startTimer(srvModel.expirationSeconds, display); // Timer
if (!validateEmail(srvModel.customerEmail)) if (srvModel.requiresRefundEmail && !validateEmail(srvModel.customerEmail))
emailForm(); // Email form Display emailForm(); // Email form Display
else else
hideEmailForm(); hideEmailForm();
@ -246,42 +243,21 @@ $(document).ready(function () {
$(".single-item-order__right__btc-price__chevron").toggleClass("expanded"); $(".single-item-order__right__btc-price__chevron").toggleClass("expanded");
}); });
// Timer Countdown // Timer Countdown && Progress bar
function startTimer(duration, display) {
var timer = duration, minutes, seconds;
var timeout = setInterval(function () {
minutes = parseInt(timer / 60, 10);
seconds = parseInt(timer % 60, 10);
minutes = minutes < 10 ? "0" + minutes : minutes;
seconds = seconds < 10 ? "0" + seconds : seconds;
display.text(minutes + ":" + seconds);
if (--timer < 0) {
clearInterval(timeout);
}
}, 1000);
}
// Progress bar
function progressStart(timerMax) { function progressStart(timerMax) {
var end = new Date(); // Setup Time Variable, should come from server var end = new Date(); // Setup Time Variable, should come from server
end.setSeconds(end.getSeconds() + srvModel.expirationSeconds); end.setSeconds(end.getSeconds() + srvModel.expirationSeconds);
timerMax *= 1000; // Usually 15 minutes = 9000 second= 900000 ms timerMax *= 1000; // Usually 15 minutes = 9000 second= 900000 ms
var timeoutVal = Math.floor(timerMax / 100); // Timeout calc
animateUpdate(); //Launch it animateUpdate(); //Launch it
function updateProgress(percentage) {
$('.timer-row__progress-bar').css("width", percentage + "%");
}
function animateUpdate() { function animateUpdate() {
var now = new Date(); var now = new Date();
var timeDiff = end.getTime() - now.getTime(); var timeDiff = end.getTime() - now.getTime();
var perc = 100 - Math.round(timeDiff / timerMax * 100); var perc = 100 - Math.round(timeDiff / timerMax * 100);
var status = checkoutCtrl.srvModel.status; var status = checkoutCtrl.srvModel.status;
updateTimer(timeDiff / 1000);
if (perc === 75 && (status === "paidPartial" || status === "new")) { if (perc === 75 && (status === "paidPartial" || status === "new")) {
$(".timer-row").addClass("expiring-soon"); $(".timer-row").addClass("expiring-soon");
checkoutCtrl.expiringSoon = true; checkoutCtrl.expiringSoon = true;
@ -289,12 +265,34 @@ $(document).ready(function () {
} }
if (perc <= 100) { if (perc <= 100) {
updateProgress(perc); updateProgress(perc);
var timeoutVal = 300; // Timeout calc
setTimeout(animateUpdate, timeoutVal); setTimeout(animateUpdate, timeoutVal);
} }
//if (perc >= 100 && status === "expired") { //if (perc >= 100 && status === "expired") {
// onDataCallback(status); // onDataCallback(status);
//} //}
} }
function updateProgress(percentage) {
$('.timer-row__progress-bar').css("width", percentage + "%");
}
function updateTimer(timer) {
var display = $(".timer-row__time-left");
if (timer >= 0) {
var minutes = parseInt(timer / 60, 10);
minutes = minutes < 10 ? "0" + minutes : minutes;
var seconds = parseInt(timer % 60, 10);
seconds = seconds < 10 ? "0" + seconds : seconds;
display.text(minutes + ":" + seconds);
} else {
display.text("00:00");
}
}
} }
// Clipboard Copy // Clipboard Copy

View File

@ -0,0 +1,48 @@
const locales_hr = {
nested: {
lang: 'Croatian'
},
"Awaiting Payment...": "Čekamo uplatu...",
"Pay with": "Plati sa",
"Contact and Refund Email": "E-mail za kontakt i povrat sredstava",
"Contact_Body": "Molimo upišite Vašu e-mail adresu. Kontaktirat ćemo Vas ukoliko bude potrebe.",
"Your email": "Vaš e-mail",
"Continue": "Dalje",
"Please enter a valid email address": "Molimo unesite ispravnu e-mail adresu",
"Order Amount": "Količina",
"Network Cost": "Trošak mreže",
"Already Paid": "Već plaćeno",
"Due": "Rok",
// Tabs
"Scan": "Skeniraj",
"Copy": "Kopiraj",
"Conversion": "Pretvori",
// Scan tab
"Open in wallet": "Otvori u novčaniku",
// Copy tab
"CompletePay_Body": "Kako bi završili uplatu pošaljite {{btcDue}} {{cryptoCode}} na navedenu adresu",
"Amount": "Iznos",
"Address": "Adresa",
"Copied": "Kopirano",
// Conversion tab
"ConversionTab_BodyTop": "Možete platiti {{btcDue}} {{cryptoCode}} pomoću altcoina koje prodavač ne podržava.",
"ConversionTab_BodyDesc": "Ovu usluga pruža treća strana. Vodite računa da nemamo kontroli nad načinom kako će Vam davatelji usluge proslijediti sredstva. Vodite računa da je račun plaćen tek kada su primljena sredstva na {{cryptoCode}} Blockchainu.",
"Shapeshift_Button_Text": "Plati s Alt-coinovima",
"ConversionTab_Lightning": "Ne postoji treća strana koja bi konvertirala Lightning Network uplate.",
// Invoice expired
"Invoice expiring soon...": "Račun uskoro ističe...",
"Invoice expired": "Račun je istekao",
"What happened?": "Što se dogodilo",
"InvoiceExpired_Body_1": "Račun je istekao i nije više valjan. Račun vrijedi samo {{maxTimeMinutes}} minuta. \
Možete se vratiti na {{storeName}}, gdje možete ponovo inicirati plaćanje.",
"InvoiceExpired_Body_2": "Ako ste pokušali poslati uplatu, ista nije registrirana na Blockchainu. Nismo još zaprimili Vašu uplatu.",
"InvoiceExpired_Body_3": "Ako poslana sredstva na budu potvrđena na Blockchainu, sredstva će biti ponovo dostupna u Vašem novčaniku.",
"Invoice ID": "Broj računa",
"Order ID": "Broj narudžbe",
"Return to StoreName": "Vrati se na {{storeName}}",
// Invoice paid
"This invoice has been paid": "Račun je plaćen",
// Invoice archived
"This invoice has been archived": "Račun je arhiviran.",
"Archived_Body": "Kontaktirajte dućan za detalje oko narudžbe ili pomoć."
};

View File

@ -47,5 +47,8 @@ const locales_is = {
"Archived_Body": "Vinsamlegast hafðu samband fyrir upplýsingar eða aðstoð.", "Archived_Body": "Vinsamlegast hafðu samband fyrir upplýsingar eða aðstoð.",
// Lightning // Lightning
"BOLT 11 Invoice": "BOLT 11 Reikningur", "BOLT 11 Invoice": "BOLT 11 Reikningur",
"Node Info": "Nótu upplýsingar" "Node Info": "Nótu upplýsingar",
//
"txCount": "{{count}} reikningur",
"txCount_plural": "{{count}} reikningar"
}; };