Compare commits

..

63 Commits

Author SHA1 Message Date
e488f93b17 Add explanation for the annoying windows popup 2019-07-12 13:04:54 +09:00
e6e9668bbb Prevent error 500 if bad psbt 2019-07-12 12:57:56 +09:00
56976898bd Fix error 414 2019-07-12 12:23:13 +09:00
221ff05c49 bump 2019-07-12 11:49:09 +09:00
8f719d3e33 Solve error 414 when PSBT are too big 2019-07-12 11:47:13 +09:00
67c2abca2d Hide openid warning message 2019-07-08 14:57:42 +09:00
36046f08f7 Use Migration startup task when starting BTCPay instead of hosted service. 2019-07-08 12:12:39 +09:00
3c4455c23c Update AppHubStreamer.cs ()
Bug fix 
2019-07-07 20:03:40 +09:00
e3db2e2b76 Remove warnings 2019-07-04 21:18:16 +09:00
5567a26b33 update translation 2019-07-04 21:16:20 +09:00
5387c3dd97 bump 2019-07-04 20:56:54 +09:00
d14eef979c Bump versions of package and software 2019-07-04 20:50:40 +09:00
c7069f4fd9 bump 2019-07-04 18:49:25 +09:00
b40239f93b bump nbxplorer 2019-07-04 18:48:54 +09:00
2958175add Sort payment requests, most recent first 2019-07-01 17:33:49 +09:00
719ad8f4d4 Add File Storage links ()
* add redirect to Services for enable provider

* add tooltip to FS FAQ
2019-07-01 15:20:33 +09:00
4055eda757 Part3: OpenIddict: Add Flows Event Handlers ()
* Part 1 & Part 2 squashed commits

pr changes


pr fixes


remove config for openid -- no need for it for now


Part 1: OpenIddict - Minor Changes & Config prep


Part2: Openiddict: Init OpenIddict & Database Migration & Auth Policies


pr changes


fix merge 


fix compile


fix compile 


Part 1: OpenIddict - Minor Changes & Config prep


add missing nuget


Part2: Openiddict: Init OpenIddict & Database Migration & Auth Policies

* Part3: OpenIddict: Add Flows Event Handlers

* pr changes

* fix merge

* fix rebase

* fix imports

* cleanup

* do not allow u2f enabled accounts to log in

* start better tests for flows

* add tests

* fixes

* reintroduce dynamic policy as policies on jwt do not work without it

* reduce logs

* fix incorrect endpoint definitions

* Add implicit flow e2e test

* add code flow and refresh flow

* do not allow jwt bearer auth for all requests( only those under /api)

* remove commentedt code

* make sure authorize attr is marked with scheme

* remove dynamic policy and set claims in jwt handler

* cleanup

* change serversettings policy to not need a claim

* Add test to checkadmin verification

* revert server setting claim removal

* fix test

* switch back to claim

* unit test fixes

* try fix build with weird references to csprojes

* start fixing rebase

* remove https requirement to handle tor

* reformat tests correctly

* fix csproj

* fix ut formatting

* PR Changes

* do not show selenium browser
2019-07-01 12:39:25 +09:00
442df56629 Merge pull request from Kukks/multiple-domains
Multiple domains for apps in BTCPay
2019-06-28 16:31:55 +09:00
477ab67fe9 set viewbag on method result instead 2019-06-26 07:02:22 +02:00
64c60741a0 Fix possible NRE 2019-06-26 13:46:45 +09:00
9e354d7703 Merge pull request from Kukks/pay-button-language
Allow language parameter in pay button endpoint
2019-06-26 13:27:52 +09:00
1932c1cd7c Merge pull request from Kukks/filesystem-downloads
fix tmp link download
2019-06-26 13:11:08 +09:00
11670d0c0f make checkout param more generic and add it to pay button generator 2019-06-25 21:01:37 +02:00
6cab02cd99 Multiple domains for apps in BTCPay
closes 
2019-06-25 20:41:32 +02:00
fc1d781272 fix tmp link download
closes 
2019-06-25 12:23:10 +02:00
a58ecfd35a Save local file storage upon selection instead
closes 
2019-06-25 11:40:33 +02:00
645516ee1b Change donation button from slider to input 2019-06-23 14:45:27 +09:00
f570de5086 Fix payment button alignement 2019-06-23 14:31:56 +09:00
81ccfa1e6c Change donation QR to use proper donation button. Remove also special thanks part as the contributors dramtically changed. 2019-06-23 14:29:43 +09:00
b808aa4971 allow language parameter in pay button endpoint 2019-06-18 18:31:20 +02:00
d1f1bc93b3 Fix version detection 2019-06-18 14:00:42 +09:00
faf433f644 Fix build 2019-06-18 13:51:04 +09:00
ba4660a03a bump 2019-06-18 13:41:30 +09:00
03aa3693d0 Update translations 2019-06-18 13:41:10 +09:00
ecae976993 Make sure we don't timeout on NBX 2019-06-18 13:37:24 +09:00
307c8980e0 Move Common and Version.csproj in Build folder 2019-06-17 21:42:48 +09:00
e53d0eda47 Fix NRE if the account has no rootedKeyPath 2019-06-16 12:32:00 +09:00
369b15b20b bump 2019-06-13 16:33:23 +09:00
ff8bbcd88a Merge branch 'amitasaurus-btcPay-coinSwitch' 2019-06-13 16:31:48 +09:00
c52d22dc30 resolved conflicts 2019-06-13 12:49:01 +05:30
4fa6c9dc3d coinswitchAmountDue always returning 1.025 possible fix 2019-06-13 12:14:28 +05:30
a958d10dd9 Fix local network detection (https://github.com/btcpayserver/btcpayserver-docker/pull/152) 2019-06-12 17:40:49 +09:00
ff86ce64b4 Merge pull request from Kukks/error-messages-login-register
Show Model errors on login/register
2019-06-12 14:02:26 +09:00
f31d8aa9d7 Merge pull request from btcpayserver/Kukks-patch-1
Fix automated docker build link
2019-06-12 14:01:18 +09:00
3cf7406123 Fix automated docker build link
Was pointing to nicolas' repo instead of the active one
2019-06-11 19:13:37 +02:00
5d8bf196a8 Fix: Allow get rate unauthenticated 2019-06-11 18:40:47 +09:00
019bd26c51 bump 2019-06-11 18:16:31 +09:00
0e1f924fc3 Relax "Insecure transport protocol to access this service, please use HTTPS or TOR" error in server setting services 2019-06-10 18:16:12 +09:00
15c3893aab Make sure currency is in uppercase 2019-06-10 00:46:29 +09:00
deeab7c238 Add link to checkout page theme doc 2019-06-09 22:26:59 +09:00
e5ba7b9e69 Refactor authentication handlers 2019-06-09 01:36:54 +09:00
ca5be7e38d Never use default AuthenticationScheme 2019-06-08 12:41:44 +09:00
fb530f2b34 fix build 2019-06-07 13:46:02 +09:00
29cbf63346 Remove deps on NetworkProvider in AppService 2019-06-07 13:40:48 +09:00
13c03cc0c2 Removing dependency on NetworkProvider from InvoiceWatcher 2019-06-07 13:34:38 +09:00
281280d3ec Fix crash which can happen during export if someone remove support for a network, inject Network inside paymentdata 2019-06-07 13:31:11 +09:00
410be51951 Update language 2019-06-07 00:49:05 +09:00
eefe8289b3 Fix exception in CreateInvoice if a payment method is not supported 2019-06-07 00:45:10 +09:00
a53a5944f8 Remove empty row if no validation 2019-06-06 18:54:51 +09:00
cd009466b6 Make sure we don't have empty row if no StatusMessage 2019-06-06 18:47:31 +09:00
f0c106de75 Change the menu nav bar pages by moving the title above the nav pills 2019-06-06 18:29:54 +09:00
fcf1b679e6 Show Model errors on login/register
Invalid logins and registrations were not showing any messages
2019-06-04 14:37:13 +02:00
77338c6054 [BUG FIX]: Coinswitch exchange with altcoins popup not showing bug fix 2019-05-02 14:49:33 +05:30
149 changed files with 2944 additions and 914 deletions
BTCPayServer.Common
BTCPayServer.Rating
BTCPayServer.Tests
BTCPayServer
Authentication/OpenId
BTCPayServer.csproj
Configuration
Controllers
Data
ExplorerClientProvider.csExtensions.cs
Extensions
HostedServices
Hosting
IStartupTask.csMigrationStartupTask.cs
Migrations
Models/StoreViewModels
Payments
Program.cs
Security
Services
Storage
Services/Providers/FileSystemStorage
StorageExtensions.cs
Validation
Views
wwwroot
Build
README.mdamd64.Dockerfilearm32v7.Dockerfilebtcpayserver.slnpublish-docker.ps1

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../Version.csproj" Condition="Exists('../Version.csproj')" />
<Import Project="../Common.csproj" />
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" />
<PackageReference Include="NBitcoin" Version="4.1.2.35" />
<PackageReference Include="NBitcoin" Version="4.1.2.37" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0.17" />
</ItemGroup>
</Project>

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../Version.csproj" Condition="Exists('../Version.csproj')" />
<Import Project="../Common.csproj" />
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<LangVersion>7.3</LangVersion>

@ -0,0 +1,368 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Primitives;
using BTCPayServer.Tests.Logging;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Xunit;
using Xunit.Abstractions;
using System.Net.Http;
using System.Net.Http.Headers;
using BTCPayServer.Data;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenIddict.Abstractions;
using OpenQA.Selenium;
namespace BTCPayServer.Tests
{
public class AuthenticationTests
{
public AuthenticationTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public async Task GetRedirectedToLoginPathOnChallenge()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var client = tester.PayTester.HttpClient;
//Wallets endpoint is protected
var response = await client.GetAsync("wallets");
var urlPath = response.RequestMessage.RequestUri.ToString()
.Replace(tester.PayTester.ServerUri.ToString(), "");
//Cookie Challenge redirects you to login page
Assert.StartsWith("Account/Login", urlPath, StringComparison.InvariantCultureIgnoreCase);
var queryString = response.RequestMessage.RequestUri.ParseQueryString();
Assert.NotNull(queryString["ReturnUrl"]);
Assert.Equal("/wallets", queryString["ReturnUrl"]);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanGetOpenIdConfiguration()
{
using (var tester = ServerTester.Create())
{
tester.Start();
using (var response =
await tester.PayTester.HttpClient.GetAsync("/.well-known/openid-configuration"))
{
using (var streamToReadFrom = new StreamReader(await response.Content.ReadAsStreamAsync()))
{
var json = await streamToReadFrom.ReadToEndAsync();
Assert.NotNull(json);
var configuration = OpenIdConnectConfiguration.Create(json);
Assert.NotNull(configuration);
}
}
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanUseNonInteractiveFlows()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var token = await RegisterPasswordClientAndGetAccessToken(user, null, tester);
await TestApiAgainstAccessToken(token, tester, user);
token = await RegisterPasswordClientAndGetAccessToken(user, "secret", tester);
await TestApiAgainstAccessToken(token, tester, user);
token = await RegisterClientCredentialsFlowAndGetAccessToken(user, "secret", tester);
await TestApiAgainstAccessToken(token, tester, user);
}
}
[Trait("Selenium", "Selenium")]
[Fact]
public async Task CanUseImplicitFlow()
{
using (var s = SeleniumTester.Create())
{
s.Start();
var tester = s.Server;
var user = tester.NewAccount();
user.GrantAccess();
var id = Guid.NewGuid().ToString();
var redirecturi = new Uri("http://127.0.0.1/oidc-callback");
var openIdClient = await user.RegisterOpenIdClient(
new OpenIddictApplicationDescriptor()
{
ClientId = id,
DisplayName = id,
Permissions = {OpenIddictConstants.Permissions.GrantTypes.Implicit},
RedirectUris = {redirecturi}
});
var implicitAuthorizeUrl = new Uri(tester.PayTester.ServerUri,
$"connect/authorize?response_type=token&client_id={id}&redirect_uri={redirecturi.AbsoluteUri}&scope=openid&nonce={Guid.NewGuid().ToString()}");
s.Driver.Navigate().GoToUrl(implicitAuthorizeUrl);
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
var url = s.Driver.Url;
var results = url.Split("#").Last().Split("&")
.ToDictionary(s1 => s1.Split("=")[0], s1 => s1.Split("=")[1]);
await TestApiAgainstAccessToken(results["access_token"], tester, user);
//in Implicit mode, you renew your token by hitting the same endpoint but adding prompt=none. If you are still logged in on the site, you will receive a fresh token.
var implicitAuthorizeUrlSilentModel = new Uri($"{implicitAuthorizeUrl.OriginalString}&prompt=none");
s.Driver.Navigate().GoToUrl(implicitAuthorizeUrl);
url = s.Driver.Url;
results = url.Split("#").Last().Split("&").ToDictionary(s1 => s1.Split("=")[0], s1 => s1.Split("=")[1]);
await TestApiAgainstAccessToken(results["access_token"], tester, user);
LogoutFlow(tester, id, s);
}
}
void LogoutFlow(ServerTester tester, string clientId, SeleniumTester seleniumTester)
{
var logoutUrl = new Uri(tester.PayTester.ServerUri,
$"connect/logout?response_type=token&client_id={clientId}");
seleniumTester.Driver.Navigate().GoToUrl(logoutUrl);
seleniumTester.GoToHome();
Assert.Throws<NoSuchElementException>(() => seleniumTester.Driver.FindElement(By.Id("Logout")));
}
[Trait("Selenium", "Selenium")]
[Fact]
public async Task CanUseCodeFlow()
{
using (var s = SeleniumTester.Create())
{
s.Start();
var tester = s.Server;
var user = tester.NewAccount();
user.GrantAccess();
var id = Guid.NewGuid().ToString();
var redirecturi = new Uri("http://127.0.0.1/oidc-callback");
var secret = "secret";
var openIdClient = await user.RegisterOpenIdClient(
new OpenIddictApplicationDescriptor()
{
ClientId = id,
DisplayName = id,
Permissions =
{
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
OpenIddictConstants.Permissions.GrantTypes.RefreshToken
},
RedirectUris = {redirecturi}
}, secret);
var authorizeUrl = new Uri(tester.PayTester.ServerUri,
$"connect/authorize?response_type=code&client_id={id}&redirect_uri={redirecturi.AbsoluteUri}&scope=openid offline_access&state={Guid.NewGuid().ToString()}");
s.Driver.Navigate().GoToUrl(authorizeUrl);
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
var url = s.Driver.Url;
var results = url.Split("?").Last().Split("&")
.ToDictionary(s1 => s1.Split("=")[0], s1 => s1.Split("=")[1]);
var httpClient = tester.PayTester.HttpClient;
var httpRequest = new HttpRequestMessage(HttpMethod.Post,
new Uri(tester.PayTester.ServerUri, "/connect/token"))
{
Content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("grant_type",
OpenIddictConstants.GrantTypes.AuthorizationCode),
new KeyValuePair<string, string>("client_id", openIdClient.ClientId),
new KeyValuePair<string, string>("client_secret", secret),
new KeyValuePair<string, string>("code", results["code"]),
new KeyValuePair<string, string>("redirect_uri", redirecturi.AbsoluteUri)
})
};
var response = await httpClient.SendAsync(httpRequest);
Assert.True(response.IsSuccessStatusCode);
string content = await response.Content.ReadAsStringAsync();
var result = JObject.Parse(content).ToObject<OpenIdConnectResponse>();
await TestApiAgainstAccessToken(result.AccessToken, tester, user);
var refreshedAccessToken = await RefreshAnAccessToken(result.RefreshToken, httpClient, id, secret);
await TestApiAgainstAccessToken(refreshedAccessToken, tester, user);
}
}
private static async Task<string> RefreshAnAccessToken(string refreshToken, HttpClient client, string clientId,
string clientSecret = null)
{
var httpRequest = new HttpRequestMessage(HttpMethod.Post,
new Uri(client.BaseAddress, "/connect/token"))
{
Content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("grant_type",
OpenIddictConstants.GrantTypes.RefreshToken),
new KeyValuePair<string, string>("client_id", clientId),
new KeyValuePair<string, string>("client_secret", clientSecret),
new KeyValuePair<string, string>("refresh_token", refreshToken)
})
};
var response = await client.SendAsync(httpRequest);
Assert.True(response.IsSuccessStatusCode);
string content = await response.Content.ReadAsStringAsync();
var result = JObject.Parse(content).ToObject<OpenIdConnectResponse>();
Assert.NotEmpty(result.AccessToken);
Assert.Null(result.Error);
return result.AccessToken;
}
private static async Task<string> RegisterClientCredentialsFlowAndGetAccessToken(TestAccount user,
string secret,
ServerTester tester)
{
var id = Guid.NewGuid().ToString();
var openIdClient = await user.RegisterOpenIdClient(
new OpenIddictApplicationDescriptor()
{
ClientId = id,
DisplayName = id,
Permissions = {OpenIddictConstants.Permissions.GrantTypes.ClientCredentials}
}, secret);
var httpClient = tester.PayTester.HttpClient;
var httpRequest = new HttpRequestMessage(HttpMethod.Post,
new Uri(tester.PayTester.ServerUri, "/connect/token"))
{
Content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("grant_type",
OpenIddictConstants.GrantTypes.ClientCredentials),
new KeyValuePair<string, string>("client_id", openIdClient.ClientId),
new KeyValuePair<string, string>("client_secret", secret)
})
};
var response = await httpClient.SendAsync(httpRequest);
Assert.True(response.IsSuccessStatusCode);
string content = await response.Content.ReadAsStringAsync();
var result = JObject.Parse(content).ToObject<OpenIdConnectResponse>();
Assert.NotEmpty(result.AccessToken);
Assert.Null(result.Error);
return result.AccessToken;
}
private static async Task<string> RegisterPasswordClientAndGetAccessToken(TestAccount user, string secret,
ServerTester tester)
{
var id = Guid.NewGuid().ToString();
var openIdClient = await user.RegisterOpenIdClient(
new OpenIddictApplicationDescriptor()
{
ClientId = id,
DisplayName = id,
Permissions = {OpenIddictConstants.Permissions.GrantTypes.Password}
}, secret);
var httpClient = tester.PayTester.HttpClient;
var httpRequest = new HttpRequestMessage(HttpMethod.Post,
new Uri(tester.PayTester.ServerUri, "/connect/token"))
{
Content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("grant_type", OpenIddictConstants.GrantTypes.Password),
new KeyValuePair<string, string>("username", user.RegisterDetails.Email),
new KeyValuePair<string, string>("password", user.RegisterDetails.Password),
new KeyValuePair<string, string>("client_id", openIdClient.ClientId),
new KeyValuePair<string, string>("client_secret", secret)
})
};
var response = await httpClient.SendAsync(httpRequest);
Assert.True(response.IsSuccessStatusCode);
string content = await response.Content.ReadAsStringAsync();
var result = JObject.Parse(content).ToObject<OpenIdConnectResponse>();
Assert.NotEmpty(result.AccessToken);
Assert.Null(result.Error);
return result.AccessToken;
}
async Task TestApiAgainstAccessToken(string accessToken, ServerTester tester, TestAccount testAccount)
{
var resultUser =
await TestApiAgainstAccessToken<string>(accessToken, "api/test/me/id",
tester.PayTester.HttpClient);
Assert.Equal(testAccount.UserId, resultUser);
var secondUser = tester.NewAccount();
secondUser.GrantAccess();
var resultStores =
await TestApiAgainstAccessToken<StoreData[]>(accessToken, "api/test/me/stores",
tester.PayTester.HttpClient);
Assert.Contains(resultStores,
data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase));
Assert.DoesNotContain(resultStores,
data => data.Id.Equals(secondUser.StoreId, StringComparison.InvariantCultureIgnoreCase));
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
$"api/test/me/stores/{testAccount.StoreId}/can-edit",
tester.PayTester.HttpClient));
Assert.Equal(testAccount.RegisterDetails.IsAdmin, await TestApiAgainstAccessToken<bool>(accessToken,
$"api/test/me/is-admin",
tester.PayTester.HttpClient));
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
{
await TestApiAgainstAccessToken<bool>(accessToken, $"api/test/me/stores/{secondUser.StoreId}/can-edit",
tester.PayTester.HttpClient);
});
}
public async Task<T> TestApiAgainstAccessToken<T>(string accessToken, string url, HttpClient client)
{
var httpRequest = new HttpRequestMessage(HttpMethod.Get,
new Uri(client.BaseAddress, url));
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var result = await client.SendAsync(httpRequest);
result.EnsureSuccessStatusCode();
var rawJson = await result.Content.ReadAsStringAsync();
if (typeof(T).IsPrimitive || typeof(T) == typeof(string))
{
return (T)Convert.ChangeType(rawJson, typeof(T));
}
return JsonConvert.DeserializeObject<T>(rawJson);
}
}
}

@ -1,4 +1,4 @@
using BTCPayServer.Configuration;
using BTCPayServer.Configuration;
using System.Linq;
using BTCPayServer.HostedServices;
using BTCPayServer.Hosting;
@ -148,7 +148,7 @@ namespace BTCPayServer.Tests
.UseKestrel()
.UseStartup<Startup>()
.Build();
_Host.Start();
_Host.StartWithTasksAsync().GetAwaiter().GetResult();
var urls = _Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses;
foreach (var url in urls)

@ -5,7 +5,7 @@ ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
WORKDIR /source
COPY Common.csproj Common.csproj
COPY Build/Common.csproj Build/Common.csproj
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer/BTCPayServer.csproj
COPY BTCPayServer.Common/BTCPayServer.Common.csproj BTCPayServer.Common/BTCPayServer.Common.csproj
COPY BTCPayServer.Rating/BTCPayServer.Rating.csproj BTCPayServer.Rating/BTCPayServer.Rating.csproj

@ -130,14 +130,22 @@ namespace BTCPayServer.Tests
Assert.Contains("Status Code: 404; Not Found", Driver.PageSource);
}
internal void GoToHome()
public void GoToHome()
{
Driver.Navigate().GoToUrl(Server.PayTester.ServerUri);
}
internal void Logout()
public void Logout()
{
Driver.FindElement(By.Id("Logout")).Click();
}
public void Login(string user, string password)
{
Driver.FindElement(By.Id("Email")).SendKeys(user);
Driver.FindElement(By.Id("Password")).SendKeys(password);
Driver.FindElement(By.Id("LoginButton")).Click();
}
}
}

@ -86,7 +86,7 @@ namespace BTCPayServer.Tests
}
}
private static void LogIn(SeleniumTester s, string email)
static void LogIn(SeleniumTester s, string email)
{
s.Driver.FindElement(By.Id("Login")).Click();
s.Driver.FindElement(By.Id("Email")).SendKeys(email);
@ -166,7 +166,7 @@ namespace BTCPayServer.Tests
}
}
private static void CreateInvoice(SeleniumTester s, string store)
static void CreateInvoice(SeleniumTester s, string store)
{
s.Driver.FindElement(By.Id("Invoices")).Click();
s.Driver.FindElement(By.Id("CreateNewInvoice")).Click();

@ -10,6 +10,7 @@ using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Authentication.OpenId.Models;
using Xunit;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Payments;
@ -18,6 +19,8 @@ using BTCPayServer.Tests.Logging;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Data;
using OpenIddict.Abstractions;
using OpenIddict.Core;
namespace BTCPayServer.Tests
{
@ -166,5 +169,14 @@ namespace BTCPayServer.Tests
if (storeController.ModelState.ErrorCount != 0)
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
}
public async Task<BTCPayOpenIdClient> RegisterOpenIdClient(OpenIddictApplicationDescriptor descriptor, string secret = null)
{
var openIddictApplicationManager = parent.PayTester.GetService<OpenIddictApplicationManager<BTCPayOpenIdClient>>();
var client = new BTCPayOpenIdClient {ApplicationUserId = UserId};
await openIddictApplicationManager.PopulateAsync(client, descriptor);
await openIddictApplicationManager.CreateAsync(client, secret);
return client;
}
}
}

@ -123,7 +123,7 @@ namespace BTCPayServer.Tests
}));
invoiceEntity.SetPaymentMethods(paymentMethods);
var btc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
var btc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
var accounting = btc.Calculate();
invoiceEntity.Payments.Add(
@ -153,14 +153,14 @@ namespace BTCPayServer.Tests
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Zero, accounting.DueUncapped);
var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
accounting = ltc.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
// LTC might have over paid due to BTC paying above what it should (round 1 satoshi up)
Assert.True(accounting.DueUncapped < Money.Zero);
var paymentMethod = InvoiceWatcher.GetNearestClearedPayment(paymentMethods, out var accounting2, null);
var paymentMethod = InvoiceWatcher.GetNearestClearedPayment(paymentMethods, out var accounting2);
Assert.Equal(btc.CryptoCode, paymentMethod.CryptoCode);
#pragma warning restore CS0618
}
@ -262,11 +262,12 @@ namespace BTCPayServer.Tests
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity()
{
Output = new TxOut(Money.Coins(0.2m), new Key()),
Accounted = true
});
entity.Payments.Add(
new PaymentEntity()
{
Output = new TxOut(Money.Coins(0.2m), new Key()),
Accounted = true
});
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
@ -276,16 +277,26 @@ namespace BTCPayServer.Tests
entity.ProductInformation = new ProductInformation() {Price = 5000};
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
paymentMethods.Add(
new PaymentMethod() {CryptoCode = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m)});
new PaymentMethod()
{
CryptoCode = "BTC",
Rate = 1000,
NextNetworkFee = Money.Coins(0.1m)
});
paymentMethods.Add(
new PaymentMethod() {CryptoCode = "LTC", Rate = 500, NextNetworkFee = Money.Coins(0.01m)});
new PaymentMethod()
{
CryptoCode = "LTC",
Rate = 500,
NextNetworkFee = Money.Coins(0.01m)
});
entity.SetPaymentMethods(paymentMethods);
entity.Payments = new List<PaymentEntity>();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(5.1m), accounting.Due);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(10.01m), accounting.TotalDue);
@ -298,7 +309,7 @@ namespace BTCPayServer.Tests
NetworkFee = 0.1m
});
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(4.2m), accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
@ -306,7 +317,7 @@ namespace BTCPayServer.Tests
Assert.Equal(Money.Coins(5.2m), accounting.TotalDue);
Assert.Equal(2, accounting.TxRequired);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 - 2.0m /* 8.21m */), accounting.Due);
Assert.Equal(Money.Coins(0.0m), accounting.CryptoPaid);
@ -321,7 +332,7 @@ namespace BTCPayServer.Tests
NetworkFee = 0.01m
});
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(4.2m - 0.5m + 0.01m / 2), accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
@ -329,7 +340,7 @@ namespace BTCPayServer.Tests
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); // The fee for LTC added
Assert.Equal(2, accounting.TxRequired);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(8.21m - 1.0m + 0.01m), accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
@ -346,7 +357,7 @@ namespace BTCPayServer.Tests
NetworkFee = 0.1m
});
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.0m) + remaining, accounting.CryptoPaid);
@ -355,7 +366,7 @@ namespace BTCPayServer.Tests
Assert.Equal(accounting.Paid, accounting.TotalDue);
Assert.Equal(2, accounting.TxRequired);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
@ -368,29 +379,6 @@ namespace BTCPayServer.Tests
#pragma warning restore CS0618
}
[Fact]
[Trait("Integration", "Integration")]
public async Task GetRedirectedToLoginPathOnChallenge()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var client = tester.PayTester.HttpClient;
//Wallets endpoint is protected
var response = await client.GetAsync("wallets");
var urlPath = response.RequestMessage.RequestUri.ToString()
.Replace(tester.PayTester.ServerUri.ToString(), "");
//Cookie Challenge redirects you to login page
Assert.StartsWith("Account/Login", urlPath, StringComparison.InvariantCultureIgnoreCase);
var queryString = response.RequestMessage.RequestUri.ParseQueryString();
Assert.NotNull(queryString["ReturnUrl"]);
Assert.Equal("/wallets", queryString["ReturnUrl"]);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanUseTestWebsiteUI()
@ -398,8 +386,7 @@ namespace BTCPayServer.Tests
using (var tester = ServerTester.Create())
{
tester.Start();
var http = new HttpClient();
var response = await http.GetAsync(tester.PayTester.ServerUri);
var response = await tester.PayTester.HttpClient.GetAsync("");
Assert.True(response.IsSuccessStatusCode);
}
}
@ -2657,27 +2644,28 @@ donation:
{
var unusedUri = new Uri("https://toto.com");
Assert.True(ExternalConnectionString.TryParse("server=/test", out var connStr, out var error));
var expanded = await connStr.Expand(new Uri("https://toto.com"), ExternalServiceTypes.Charge);
var expanded = await connStr.Expand(new Uri("https://toto.com"), ExternalServiceTypes.Charge, NetworkType.Mainnet);
Assert.Equal(new Uri("https://toto.com/test"), expanded.Server);
expanded = await connStr.Expand(new Uri("http://toto.onion"), ExternalServiceTypes.Charge);
expanded = await connStr.Expand(new Uri("http://toto.onion"), ExternalServiceTypes.Charge, NetworkType.Mainnet);
Assert.Equal(new Uri("http://toto.onion/test"), expanded.Server);
await Assert.ThrowsAsync<SecurityException>(() => connStr.Expand(new Uri("http://toto.com"), ExternalServiceTypes.Charge));
await Assert.ThrowsAsync<SecurityException>(() => connStr.Expand(new Uri("http://toto.com"), ExternalServiceTypes.Charge, NetworkType.Mainnet));
await connStr.Expand(new Uri("http://toto.com"), ExternalServiceTypes.Charge, NetworkType.Testnet);
// Make sure absolute paths are not expanded
Assert.True(ExternalConnectionString.TryParse("server=https://tow/test", out connStr, out error));
expanded = await connStr.Expand(new Uri("https://toto.com"), ExternalServiceTypes.Charge);
expanded = await connStr.Expand(new Uri("https://toto.com"), ExternalServiceTypes.Charge, NetworkType.Mainnet);
Assert.Equal(new Uri("https://tow/test"), expanded.Server);
// Error if directory not exists
Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};macaroondirectorypath=pouet", out connStr, out error));
await Assert.ThrowsAsync<DirectoryNotFoundException>(() => connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC));
await Assert.ThrowsAsync<DirectoryNotFoundException>(() => connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest));
await connStr.Expand(unusedUri, ExternalServiceTypes.Charge);
await Assert.ThrowsAsync<DirectoryNotFoundException>(() => connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC, NetworkType.Mainnet));
await Assert.ThrowsAsync<DirectoryNotFoundException>(() => connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest, NetworkType.Mainnet));
await connStr.Expand(unusedUri, ExternalServiceTypes.Charge, NetworkType.Mainnet);
var macaroonDirectory = CreateDirectory();
Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};macaroondirectorypath={macaroonDirectory}", out connStr, out error));
await connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC);
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest);
await connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC, NetworkType.Mainnet);
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest, NetworkType.Mainnet);
Assert.NotNull(expanded.Macaroons);
Assert.Null(expanded.MacaroonFilePath);
Assert.Null(expanded.Macaroons.AdminMacaroon);
@ -2687,7 +2675,7 @@ donation:
File.WriteAllBytes($"{macaroonDirectory}/admin.macaroon", new byte[] { 0xaa });
File.WriteAllBytes($"{macaroonDirectory}/invoice.macaroon", new byte[] { 0xab });
File.WriteAllBytes($"{macaroonDirectory}/readonly.macaroon", new byte[] { 0xac });
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest);
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest, NetworkType.Mainnet);
Assert.NotNull(expanded.Macaroons.AdminMacaroon);
Assert.NotNull(expanded.Macaroons.InvoiceMacaroon);
Assert.Equal("ab", expanded.Macaroons.InvoiceMacaroon.Hex);
@ -2696,7 +2684,7 @@ donation:
Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};cookiefilepath={macaroonDirectory}/charge.cookie", out connStr, out error));
File.WriteAllText($"{macaroonDirectory}/charge.cookie", "apitoken");
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.Charge);
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.Charge, NetworkType.Mainnet);
Assert.Equal("apitoken", expanded.APIToken);
}

@ -71,7 +71,7 @@ services:
nbxplorer:
image: nicolasdorier/nbxplorer:2.0.0.48
image: nicolasdorier/nbxplorer:2.0.0.52
restart: unless-stopped
ports:
- "32838:32838"
@ -227,7 +227,7 @@ services:
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
merchant_lnd:
image: btcpayserver/lnd:v0.6-beta
image: btcpayserver/lnd:v0.7.0-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -258,7 +258,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.6-beta
image: btcpayserver/lnd:v0.7.0-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"

@ -0,0 +1,21 @@
using AspNet.Security.OpenIdConnect.Primitives;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Authentication.OpenId
{
public class AuthorizationCodeGrantTypeEventHandler : OpenIdGrantHandlerCheckCanSignIn
{
public AuthorizationCodeGrantTypeEventHandler(SignInManager<ApplicationUser> signInManager,
IOptions<IdentityOptions> identityOptions, UserManager<ApplicationUser> userManager) : base(signInManager,
identityOptions, userManager)
{
}
protected override bool IsValid(OpenIdConnectRequest request)
{
return request.IsAuthorizationCodeGrantType();
}
}
}

@ -0,0 +1,78 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Primitives;
using BTCPayServer.Models;
using BTCPayServer.Security;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using OpenIddict.Server;
namespace BTCPayServer.Authentication.OpenId
{
public class AuthorizationEventHandler : BaseOpenIdGrantHandler<OpenIddictServerEvents.HandleAuthorizationRequest>
{
private readonly UserManager<ApplicationUser> _userManager;
public override async Task<OpenIddictServerEventState> HandleAsync(
OpenIddictServerEvents.HandleAuthorizationRequest notification)
{
if (!notification.Context.Request.IsAuthorizationRequest())
{
return OpenIddictServerEventState.Unhandled;
}
var auth = await notification.Context.HttpContext.AuthenticateAsync();
if (!auth.Succeeded)
{
// If the client application request promptless authentication,
// return an error indicating that the user is not logged in.
if (notification.Context.Request.HasPrompt(OpenIdConnectConstants.Prompts.None))
{
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIdConnectConstants.Properties.Error] = OpenIdConnectConstants.Errors.LoginRequired,
[OpenIdConnectConstants.Properties.ErrorDescription] = "The user is not logged in."
});
// Ask OpenIddict to return a login_required error to the client application.
await notification.Context.HttpContext.ForbidAsync(properties);
notification.Context.HandleResponse();
return OpenIddictServerEventState.Handled;
}
await notification.Context.HttpContext.ChallengeAsync();
notification.Context.HandleResponse();
return OpenIddictServerEventState.Handled;
}
// Retrieve the profile of the logged in user.
var user = await _userManager.GetUserAsync(auth.Principal);
if (user == null)
{
notification.Context.Reject(
error: OpenIddictConstants.Errors.InvalidGrant,
description: "An internal error has occurred");
return OpenIddictServerEventState.Handled;
}
// Create a new authentication ticket.
var ticket = await CreateTicketAsync(notification.Context.Request, user);
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
notification.Context.Validate(ticket);
return OpenIddictServerEventState.Handled;
}
public AuthorizationEventHandler(
UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager,
IOptions<IdentityOptions> identityOptions) : base(signInManager, identityOptions)
{
_userManager = userManager;
}
}
}

@ -0,0 +1,104 @@
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using OpenIddict.Server;
namespace BTCPayServer.Authentication.OpenId
{
public abstract class BaseOpenIdGrantHandler<T> : IOpenIddictServerEventHandler<T>
where T : class, IOpenIddictServerEvent
{
protected readonly SignInManager<ApplicationUser> _signInManager;
protected readonly IOptions<IdentityOptions> _identityOptions;
protected BaseOpenIdGrantHandler(SignInManager<ApplicationUser> signInManager,
IOptions<IdentityOptions> identityOptions)
{
_signInManager = signInManager;
_identityOptions = identityOptions;
}
protected async Task<AuthenticationTicket> CreateTicketAsync(
OpenIdConnectRequest request, ApplicationUser user,
AuthenticationProperties properties = null)
{
// Create a new ClaimsPrincipal containing the claims that
// will be used to create an id_token, a token or a code.
var principal = await _signInManager.CreateUserPrincipalAsync(user);
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(principal, properties,
OpenIddictServerDefaults.AuthenticationScheme);
if (!request.IsAuthorizationCodeGrantType() && !request.IsRefreshTokenGrantType())
{
// Note: in this sample, the granted scopes match the requested scope
// but you may want to allow the user to uncheck specific scopes.
// For that, simply restrict the list of scopes before calling SetScopes.
ticket.SetScopes(request.GetScopes());
}
foreach (var claim in ticket.Principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, ticket));
}
return ticket;
}
private IEnumerable<string> GetDestinations(Claim claim, AuthenticationTicket ticket)
{
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
switch (claim.Type)
{
case OpenIddictConstants.Claims.Name:
yield return OpenIddictConstants.Destinations.AccessToken;
if (ticket.HasScope(OpenIddictConstants.Scopes.Profile))
yield return OpenIddictConstants.Destinations.IdentityToken;
yield break;
case OpenIddictConstants.Claims.Email:
yield return OpenIddictConstants.Destinations.AccessToken;
if (ticket.HasScope(OpenIddictConstants.Scopes.Email))
yield return OpenIddictConstants.Destinations.IdentityToken;
yield break;
case OpenIddictConstants.Claims.Role:
yield return OpenIddictConstants.Destinations.AccessToken;
if (ticket.HasScope(OpenIddictConstants.Scopes.Roles))
yield return OpenIddictConstants.Destinations.IdentityToken;
yield break;
default:
if (claim.Type == _identityOptions.Value.ClaimsIdentity.SecurityStampClaimType)
{
// Never include the security stamp in the access and identity tokens, as it's a secret value.
yield break;
}
else
{
yield return OpenIddictConstants.Destinations.AccessToken;
yield break;
}
}
}
public abstract Task<OpenIddictServerEventState> HandleAsync(T notification);
}
}

@ -0,0 +1,61 @@
using System.Security.Claims;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using BTCPayServer.Authentication.OpenId.Models;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using OpenIddict.Core;
using OpenIddict.EntityFrameworkCore.Models;
using OpenIddict.Server;
namespace BTCPayServer.Authentication.OpenId
{
public class
ClientCredentialsGrantTypeEventHandler : BaseOpenIdGrantHandler<OpenIddictServerEvents.HandleTokenRequest>
{
private readonly OpenIddictApplicationManager<BTCPayOpenIdClient> _applicationManager;
private readonly UserManager<ApplicationUser> _userManager;
public ClientCredentialsGrantTypeEventHandler(SignInManager<ApplicationUser> signInManager,
OpenIddictApplicationManager<BTCPayOpenIdClient> applicationManager,
IOptions<IdentityOptions> identityOptions, UserManager<ApplicationUser> userManager) : base(signInManager,
identityOptions)
{
_applicationManager = applicationManager;
_userManager = userManager;
}
public override async Task<OpenIddictServerEventState> HandleAsync(
OpenIddictServerEvents.HandleTokenRequest notification)
{
var request = notification.Context.Request;
if (!request.IsClientCredentialsGrantType())
{
// Allow other handlers to process the event.
return OpenIddictServerEventState.Unhandled;
}
var application = await _applicationManager.FindByClientIdAsync(request.ClientId,
notification.Context.HttpContext.RequestAborted);
if (application == null)
{
notification.Context.Reject(
error: OpenIddictConstants.Errors.InvalidClient,
description: "The client application was not found in the database.");
// Don't allow other handlers to process the event.
return OpenIddictServerEventState.Handled;
}
var user = await _userManager.FindByIdAsync(application.ApplicationUserId);
notification.Context.Validate(await CreateTicketAsync(request, user));
// Don't allow other handlers to process the event.
return OpenIddictServerEventState.Handled;
}
}
}

@ -0,0 +1,32 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using OpenIddict.Server;
namespace BTCPayServer.Authentication.OpenId
{
public class LogoutEventHandler: BaseOpenIdGrantHandler<OpenIddictServerEvents.HandleLogoutRequest>
{
public LogoutEventHandler(SignInManager<ApplicationUser> signInManager, IOptions<IdentityOptions> identityOptions) : base(signInManager, identityOptions)
{
}
public override async Task<OpenIddictServerEventState> HandleAsync(OpenIddictServerEvents.HandleLogoutRequest notification)
{
// Ask ASP.NET Core Identity to delete the local and external cookies created
// when the user agent is redirected from the external identity provider
// after a successful authentication flow (e.g Google or Facebook).
await _signInManager.SignOutAsync();
// Returning a SignOutResult will ask OpenIddict to redirect the user agent
// to the post_logout_redirect_uri specified by the client application.
await notification.Context.HttpContext.SignOutAsync(OpenIddictServerDefaults.AuthenticationScheme);
notification.Context.HandleResponse();
return OpenIddictServerEventState.Handled;
}
}
}

@ -0,0 +1,65 @@
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Primitives;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using OpenIddict.Server;
namespace BTCPayServer.Authentication.OpenId
{
public abstract class
OpenIdGrantHandlerCheckCanSignIn : BaseOpenIdGrantHandler<OpenIddictServerEvents.HandleTokenRequest>
{
private readonly UserManager<ApplicationUser> _userManager;
protected OpenIdGrantHandlerCheckCanSignIn(SignInManager<ApplicationUser> signInManager,
IOptions<IdentityOptions> identityOptions, UserManager<ApplicationUser> userManager) : base(signInManager,
identityOptions)
{
_userManager = userManager;
}
protected abstract bool IsValid(OpenIdConnectRequest request);
public override async Task<OpenIddictServerEventState> HandleAsync(
OpenIddictServerEvents.HandleTokenRequest notification)
{
var request = notification.Context.Request;
if (!IsValid(request))
{
// Allow other handlers to process the event.
return OpenIddictServerEventState.Unhandled;
}
var scheme = notification.Context.Scheme.Name;
var authenticateResult = (await notification.Context.HttpContext.AuthenticateAsync(scheme));
var user = await _userManager.GetUserAsync(authenticateResult.Principal);
if (user == null)
{
notification.Context.Reject(
error: OpenIddictConstants.Errors.InvalidGrant,
description: "The token is no longer valid.");
// Don't allow other handlers to process the event.
return OpenIddictServerEventState.Handled;
}
// Ensure the user is still allowed to sign in.
if (!await _signInManager.CanSignInAsync(user))
{
notification.Context.Reject(
error: OpenIddictConstants.Errors.InvalidGrant,
description: "The user is no longer allowed to sign in.");
// Don't allow other handlers to process the event.
return OpenIddictServerEventState.Handled;
}
notification.Context.Validate(await CreateTicketAsync(request, user));
// Don't allow other handlers to process the event.
return OpenIddictServerEventState.Handled;
}
}
}

@ -0,0 +1,57 @@
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Primitives;
using BTCPayServer.Models;
using BTCPayServer.Services.U2F;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using OpenIddict.Server;
namespace BTCPayServer.Authentication.OpenId
{
public class PasswordGrantTypeEventHandler : BaseOpenIdGrantHandler<OpenIddictServerEvents.HandleTokenRequest>
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly U2FService _u2FService;
public PasswordGrantTypeEventHandler(SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager,
IOptions<IdentityOptions> identityOptions, U2FService u2FService) : base(signInManager, identityOptions)
{
_userManager = userManager;
_u2FService = u2FService;
}
public override async Task<OpenIddictServerEventState> HandleAsync(
OpenIddictServerEvents.HandleTokenRequest notification)
{
var request = notification.Context.Request;
if (!request.IsPasswordGrantType())
{
// Allow other handlers to process the event.
return OpenIddictServerEventState.Unhandled;
}
// Validate the user credentials.
// Note: to mitigate brute force attacks, you SHOULD strongly consider
// applying a key derivation function like PBKDF2 to slow down
// the password validation process. You SHOULD also consider
// using a time-constant comparer to prevent timing attacks.
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null || await _u2FService.HasDevices(user.Id) ||
!(await _signInManager.CheckPasswordSignInAsync(user, request.Password, lockoutOnFailure: true))
.Succeeded)
{
notification.Context.Reject(
error: OpenIddictConstants.Errors.InvalidGrant,
description: "The specified credentials are invalid.");
// Don't allow other handlers to process the event.
return OpenIddictServerEventState.Handled;
}
notification.Context.Validate(await CreateTicketAsync(request, user));
// Don't allow other handlers to process the event.
return OpenIddictServerEventState.Handled;
}
}
}

@ -0,0 +1,21 @@
using AspNet.Security.OpenIdConnect.Primitives;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Authentication.OpenId
{
public class RefreshTokenGrantTypeEventHandler : OpenIdGrantHandlerCheckCanSignIn
{
public RefreshTokenGrantTypeEventHandler(SignInManager<ApplicationUser> signInManager,
IOptions<IdentityOptions> identityOptions, UserManager<ApplicationUser> userManager) : base(signInManager,
identityOptions, userManager)
{
}
protected override bool IsValid(OpenIdConnectRequest request)
{
return request.IsRefreshTokenGrantType();
}
}
}

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../Version.csproj" Condition="Exists('../Version.csproj')" />
<Import Project="../Common.csproj" />
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
@ -30,7 +30,7 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.19" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.22" />
<PackageReference Include="BuildBundlerMinifier" Version="2.9.406" />
<PackageReference Include="BundlerMinifier.Core" Version="2.9.406" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="2.9.406" />

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using NBitcoin;
namespace BTCPayServer.Configuration
{
@ -30,13 +31,16 @@ namespace BTCPayServer.Configuration
/// Return a connectionString which does not depends on external resources or information like relative path or file path
/// </summary>
/// <returns></returns>
public async Task<ExternalConnectionString> Expand(Uri absoluteUrlBase, ExternalServiceTypes serviceType)
public async Task<ExternalConnectionString> Expand(Uri absoluteUrlBase, ExternalServiceTypes serviceType, NetworkType network)
{
var connectionString = this.Clone();
// Transform relative URI into absolute URI
var serviceUri = connectionString.Server.IsAbsoluteUri ? connectionString.Server : ToRelative(absoluteUrlBase, connectionString.Server.ToString());
if (!serviceUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) &&
!serviceUri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase))
var isSecure = network != NetworkType.Mainnet ||
serviceUri.Scheme == "https" ||
serviceUri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase) ||
Extensions.IsLocalNetwork(serviceUri.DnsSafeHost);
if (!isSecure)
{
throw new System.Security.SecurityException($"Insecure transport protocol to access this service, please use HTTPS or TOR");
}

@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Models;
@ -10,6 +11,7 @@ using NBitcoin;
using Newtonsoft.Json;
using BTCPayServer.Services;
using BTCPayServer.HostedServices;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Controllers
{
@ -25,37 +27,58 @@ namespace BTCPayServer.Controllers
_cachedServerSettings = cachedServerSettings;
}
private async Task<ViewResult> GoToApp(string appId, AppType? appType)
{
if (appType.HasValue && !string.IsNullOrEmpty(appId))
{
switch (appType.Value)
{
case AppType.Crowdfund:
{
var serviceProvider = HttpContext.RequestServices;
var controller = (AppsPublicController)serviceProvider.GetService(typeof(AppsPublicController));
controller.Url = Url;
controller.ControllerContext = ControllerContext;
var res = await controller.ViewCrowdfund(appId, null) as ViewResult;
if (res != null)
{
res.ViewName = "/Views/AppsPublic/ViewCrowdfund.cshtml";
return res; // return
}
break;
}
case AppType.PointOfSale:
{
var serviceProvider = HttpContext.RequestServices;
var controller = (AppsPublicController)serviceProvider.GetService(typeof(AppsPublicController));
controller.Url = Url;
controller.ControllerContext = ControllerContext;
var res = await controller.ViewPointOfSale(appId) as ViewResult;
if (res != null)
{
res.ViewName = "/Views/AppsPublic/ViewPointOfSale.cshtml";
return res; // return
}
break;
}
}
}
return null;
}
public async Task<IActionResult> Index()
{
if (_cachedServerSettings.RootAppType is Services.Apps.AppType.Crowdfund)
var matchedDomainMapping = _cachedServerSettings.DomainToAppMapping.FirstOrDefault(item =>
item.Domain.Equals(Request.Host.Host, StringComparison.InvariantCultureIgnoreCase));
if (matchedDomainMapping != null)
{
var serviceProvider = HttpContext.RequestServices;
var controller = (AppsPublicController)serviceProvider.GetService(typeof(AppsPublicController));
controller.Url = Url;
controller.ControllerContext = ControllerContext;
var res = await controller.ViewCrowdfund(_cachedServerSettings.RootAppId, null) as ViewResult;
if (res != null)
{
res.ViewName = "/Views/AppsPublic/ViewCrowdfund.cshtml";
return res; // return
}
}
else if (_cachedServerSettings.RootAppType is Services.Apps.AppType.PointOfSale)
{
var serviceProvider = HttpContext.RequestServices;
var controller = (AppsPublicController)serviceProvider.GetService(typeof(AppsPublicController));
controller.Url = Url;
controller.ControllerContext = ControllerContext;
var res = await controller.ViewPointOfSale(_cachedServerSettings.RootAppId) as ViewResult;
if (res != null)
{
res.ViewName = "/Views/AppsPublic/ViewPointOfSale.cshtml";
return res; // return
}
return await GoToApp(matchedDomainMapping.AppId, matchedDomainMapping.AppType) ?? View("Home");
}
return View("Home");
return await GoToApp(_cachedServerSettings.RootAppId, _cachedServerSettings.RootAppType) ?? View("Home");
}
[Route("translate")]
@ -116,20 +139,6 @@ namespace BTCPayServer.Controllers
return View(vm);
}
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}
public IActionResult Contact()
{
ViewData["Message"] = "Your contact page.";
return View();
}
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });

@ -111,23 +111,18 @@ namespace BTCPayServer.Controllers
foreach (var payment in invoice.GetPayments())
{
var paymentNetwork = _NetworkProvider.GetNetwork<BTCPayNetwork>(payment.GetCryptoCode());
if (paymentNetwork == null)
{
continue;
}
var paymentData = payment.GetCryptoPaymentData();
//TODO: abstract
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
{
var m = new InvoiceDetailsModel.Payment();
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
m.DepositAddress = onChainPaymentData.GetDestination(paymentNetwork);
m.DepositAddress = onChainPaymentData.GetDestination();
int confirmationCount = onChainPaymentData.ConfirmationCount;
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
if (confirmationCount >= payment.Network.MaxTrackedConfirmation)
{
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
m.Confirmations = "At least " + (payment.Network.MaxTrackedConfirmation);
}
else
{
@ -136,7 +131,7 @@ namespace BTCPayServer.Controllers
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, payment.Network.BlockExplorerLink, m.TransactionId);
m.Replaced = !payment.Accounted;
model.OnChainPayments.Add(m);
}
@ -145,7 +140,7 @@ namespace BTCPayServer.Controllers
var lightningPaymentData = (LightningLikePaymentData)paymentData;
model.OffChainPayments.Add(new InvoiceDetailsModel.OffChainPayment()
{
Crypto = paymentNetwork.CryptoCode,
Crypto = payment.Network.CryptoCode,
BOLT11 = lightningPaymentData.BOLT11
});
}
@ -242,7 +237,7 @@ namespace BTCPayServer.Controllers
paymentMethodId = paymentMethodTemp.GetId();
}
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId);
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
var dto = invoice.EntityToDTO();
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
@ -499,7 +494,7 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint(false)]
public async Task<IActionResult> Export(string format, string searchTerm = null, int timezoneOffset = 0)
{
var model = new InvoiceExport(_NetworkProvider, _CurrencyNameTable);
var model = new InvoiceExport(_CurrencyNameTable);
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm, timezoneOffset);
invoiceQuery.Skip = 0;

@ -67,6 +67,7 @@ namespace BTCPayServer.Controllers
{
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
throw new UnauthorizedAccessException();
invoice.Currency = invoice.Currency?.ToUpperInvariant() ?? "USD";
InvoiceLogs logs = new InvoiceLogs();
logs.Write("Creation of invoice starting");
var entity = _InvoiceRepository.CreateNewInvoice();
@ -158,7 +159,7 @@ namespace BTCPayServer.Controllers
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules, cancellationToken);
var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair);
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !excludeFilter.Match(s.PaymentId))
.Where(s => !excludeFilter.Match(s.PaymentId) && _paymentMethodHandlerDictionary.Support(s.PaymentId))
.Select(c =>
(Handler: _paymentMethodHandlerDictionary[c.PaymentId],
SupportedPaymentMethod: c,

@ -161,11 +161,15 @@ namespace BTCPayServer.Controllers
blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts;
data.SetBlob(blob);
if (string.IsNullOrEmpty(id))
{
data.Created = DateTimeOffset.UtcNow;
}
data = await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(data);
_EventAggregator.Publish(new PaymentRequestUpdated()
{
Data = data,
PaymentRequestId = data.Id
PaymentRequestId = data.Id,
});
return RedirectToAction("EditPaymentRequest", new {id = data.Id, StatusMessage = "Saved"});

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
@ -58,7 +59,17 @@ namespace BTCPayServer.Controllers
RedirectURL = model.BrowserRedirect,
FullNotifications = true
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
return Redirect(invoice.Data.Url);
if (string.IsNullOrEmpty(model.CheckoutQueryString))
{
return Redirect(invoice.Data.Url);
}
var additionalParamValues = HttpUtility.ParseQueryString(model.CheckoutQueryString);
var uriBuilder = new UriBuilder(invoice.Data.Url);
var paramValues = HttpUtility.ParseQueryString(uriBuilder.Query);
paramValues.Add(additionalParamValues);
uriBuilder.Query = paramValues.ToString();
return Redirect(uriBuilder.Uri.AbsoluteUri);
}
}
}

@ -0,0 +1,61 @@
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Security;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Validation;
namespace BTCPayServer.Controllers.RestApi
{
/// <summary>
/// this controller serves as a testing endpoint for our OpenId unit tests
/// </summary>
[Route("api/[controller]")]
[ApiController]
[Authorize(AuthenticationSchemes = OpenIddictValidationDefaults.AuthenticationScheme)]
public class TestController : ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly StoreRepository _storeRepository;
public TestController(UserManager<ApplicationUser> userManager, StoreRepository storeRepository)
{
_userManager = userManager;
_storeRepository = storeRepository;
}
[HttpGet("me/id")]
public string GetCurrentUserId()
{
return _userManager.GetUserId(User);
}
[HttpGet("me")]
public async Task<ApplicationUser> GetCurrentUser()
{
return await _userManager.GetUserAsync(User);
}
[HttpGet("me/is-admin")]
public bool AmIAnAdmin()
{
return User.IsInRole(Roles.ServerAdmin);
}
[HttpGet("me/stores")]
public async Task<StoreData[]> GetCurrentUserStores()
{
return await _storeRepository.GetStoresByUserId(_userManager.GetUserId(User));
}
[HttpGet("me/stores/{storeId}/can-edit")]
[Authorize(Policy = Policies.CanModifyStoreSettings.Key, AuthenticationSchemes = OpenIddictValidationDefaults.AuthenticationScheme)]
public bool CanEdit(string storeId)
{
return true;
}
}
}

@ -235,6 +235,12 @@ namespace BTCPayServer.Controllers
fileProviderService.GetProviderConfiguration(data));
case FileSystemFileProviderService fileProviderService:
if (data.Provider != BTCPayServer.Storage.Models.StorageProvider.FileSystem)
{
_ = await SaveStorageProvider(new FileSystemStorageConfiguration(),
BTCPayServer.Storage.Models.StorageProvider.FileSystem);
}
return View(nameof(EditFileSystemStorageProvider),
fileProviderService.GetProviderConfiguration(data));
}

@ -17,6 +17,7 @@ using NBitcoin.DataEncoders;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@ -33,6 +34,7 @@ using BTCPayServer.Storage.Services.Providers;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Mvc.Rendering;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.Controllers
{
@ -459,43 +461,61 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> Policies()
{
var data = (await _SettingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
// load display app dropdown
using (var ctx = _ContextFactory.CreateContext())
{
var userId = _UserManager.GetUserId(base.User);
var selectList = ctx.Users.Where(user => user.Id == userId)
.SelectMany(s => s.UserStores)
.Select(s => s.StoreData)
.SelectMany(s => s.Apps)
.Select(a => new SelectListItem($"{a.AppType} - {a.Name}", a.Id)).ToList();
selectList.Insert(0, new SelectListItem("(None)", null));
ViewBag.AppsList = new SelectList(selectList, "Value", "Text", data.RootAppId);
}
ViewBag.AppsList = await GetAppSelectList();
return View(data);
}
[Route("server/policies")]
[HttpPost]
public async Task<IActionResult> Policies(PoliciesSettings settings)
public async Task<IActionResult> Policies(PoliciesSettings settings, string command = "")
{
if (!String.IsNullOrEmpty(settings.RootAppId))
ViewBag.AppsList = await GetAppSelectList();
if (command == "add-domain")
{
using (var ctx = _ContextFactory.CreateContext())
{
var app = ctx.Apps.SingleOrDefault(a => a.Id == settings.RootAppId);
if (app != null)
settings.RootAppType = Enum.Parse<AppType>(app.AppType);
else
settings.RootAppType = null;
}
ModelState.Clear();
settings.DomainToAppMapping.Add(new PoliciesSettings.DomainToAppMappingItem());
return View(settings);
}
if (command.StartsWith("remove-domain", StringComparison.InvariantCultureIgnoreCase))
{
ModelState.Clear();
var index = int.Parse(command.Substring(command.IndexOf(":",StringComparison.InvariantCultureIgnoreCase) + 1), CultureInfo.InvariantCulture);
settings.DomainToAppMapping.RemoveAt(index);
return View(settings);
}
if (!ModelState.IsValid)
{
return View(settings);
}
var appIdsToFetch = settings.DomainToAppMapping.Select(item => item.AppId).ToList();
if (!string.IsNullOrEmpty(settings.RootAppId))
{
appIdsToFetch.Add(settings.RootAppId);
}
else
{
// not preserved on client side, but clearing it just in case
settings.RootAppType = null;
}
if (appIdsToFetch.Any())
{
using (var ctx = _ContextFactory.CreateContext())
{
var apps = await ctx.Apps.Where(data => appIdsToFetch.Contains(data.Id))
.ToDictionaryAsync(data => data.Id, data => Enum.Parse<AppType>(data.AppType));
if (!string.IsNullOrEmpty(settings.RootAppId))
{
settings.RootAppType = apps[settings.RootAppId];
}
foreach (var domainToAppMappingItem in settings.DomainToAppMapping)
{
domainToAppMappingItem.AppType = apps[domainToAppMappingItem.AppId];
}
}
}
await _SettingsRepository.UpdateSetting(settings);
TempData["StatusMessage"] = "Policies updated successfully";
return RedirectToAction(nameof(Policies));
@ -555,6 +575,22 @@ namespace BTCPayServer.Controllers
return View(result);
}
private async Task<List<SelectListItem>> GetAppSelectList()
{
// load display app dropdown
using (var ctx = _ContextFactory.CreateContext())
{
var userId = _UserManager.GetUserId(base.User);
var selectList = await ctx.Users.Where(user => user.Id == userId)
.SelectMany(s => s.UserStores)
.Select(s => s.StoreData)
.SelectMany(s => s.Apps)
.Select(a => new SelectListItem($"{a.AppType} - {a.Name}", a.Id)).ToListAsync();
selectList.Insert(0, new SelectListItem("(None)", null));
return selectList;
}
}
private static bool TryParseAsExternalService(TorService torService, out ExternalService externalService)
{
externalService = null;
@ -604,7 +640,7 @@ namespace BTCPayServer.Controllers
ServiceLink = service.ConnectionString.Server.AbsoluteUri.WithoutEndingSlash()
});
}
var connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type);
var connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type, _Options.NetworkType);
switch (service.Type)
{
case ExternalServiceTypes.Charge:
@ -720,7 +756,7 @@ namespace BTCPayServer.Controllers
ExternalConnectionString connectionString = null;
try
{
connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type);
connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type, _Options.NetworkType);
}
catch (Exception ex)
{

@ -35,7 +35,7 @@ namespace BTCPayServer.Controllers
{
[Route("stores")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[Authorize(Policy = Policies.CanModifyStoreSettings.Key)]
[Authorize(Policy = Policies.CanModifyStoreSettings.Key, AuthenticationSchemes = Policies.CookieAuthentication)]
[AutoValidateAntiforgeryToken]
public partial class StoresController : Controller
{

@ -141,7 +141,11 @@ namespace BTCPayServer.Controllers
var derivationSchemeSettings = await GetDerivationSchemeSettings(walletId);
if (derivationSchemeSettings == null)
return NotFound();
await FetchTransactionDetails(derivationSchemeSettings, vm, network);
try
{
await FetchTransactionDetails(derivationSchemeSettings, vm, network);
}
catch { return BadRequest(); }
return View(nameof(WalletPSBTReady), vm);
}

@ -410,7 +410,7 @@ namespace BTCPayServer.Controllers
}
psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath);
ModelState.Remove(nameof(viewModel.PSBT));
return await WalletPSBTReady(walletId, psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath.ToString());
return await WalletPSBTReady(walletId, psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath?.ToString());
}
private string ValueToString(Money v, BTCPayNetworkBase network)
@ -488,7 +488,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("{walletId}/rescan")]
[Authorize(Policy = Policies.CanModifyServerSettings.Key)]
[Authorize(Policy = Policies.CanModifyServerSettings.Key, AuthenticationSchemes = Policies.CookieAuthentication)]
public async Task<IActionResult> WalletRescan(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, RescanWalletModel vm)

@ -224,6 +224,9 @@ namespace BTCPayServer.Data
.HasOne(o => o.StoreData)
.WithMany(i => i.PaymentRequests)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<PaymentRequestData>()
.Property(e => e.Created)
.HasDefaultValue(NBitcoin.Utils.UnixTimeToDateTime(0));
builder.Entity<PaymentRequestData>()
.HasIndex(o => o.Status);

@ -146,7 +146,7 @@ namespace BTCPayServer.Data
}
}
if (!existing && supportedPaymentMethod == null && supportedPaymentMethod.PaymentId.IsBTCOnChain)
if (!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain)
{
DerivationStrategy = null;
}

@ -33,7 +33,7 @@ namespace BTCPayServer
Logs.Configuration.LogInformation($"{setting.CryptoCode}: Cookie file is {(setting.CookieFile ?? "not set")}");
if (setting.ExplorerUri != null)
{
_Clients.TryAdd(setting.CryptoCode, CreateExplorerClient(httpClientFactory.CreateClient($"NBXPLORER_{setting.CryptoCode}"), _NetworkProviders.GetNetwork<BTCPayNetwork>(setting.CryptoCode), setting.ExplorerUri, setting.CookieFile));
_Clients.TryAdd(setting.CryptoCode, CreateExplorerClient(httpClientFactory.CreateClient(nameof(ExplorerClientProvider)), _NetworkProviders.GetNetwork<BTCPayNetwork>(setting.CryptoCode), setting.ExplorerUri, setting.CookieFile));
}
}
}

@ -34,11 +34,29 @@ using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using NBXplorer.DerivationStrategy;
using System.Net;
using Microsoft.AspNetCore.Hosting;
namespace BTCPayServer
{
public static class Extensions
{
public static IServiceCollection AddStartupTask<T>(this IServiceCollection services)
where T : class, IStartupTask
=> services.AddTransient<IStartupTask, T>();
public static async Task StartWithTasksAsync(this IWebHost webHost, CancellationToken cancellationToken = default)
{
// Load all tasks from DI
var startupTasks = webHost.Services.GetServices<IStartupTask>();
// Execute all the tasks
foreach (var startupTask in startupTasks)
{
await startupTask.ExecuteAsync(cancellationToken).ConfigureAwait(false);
}
// Start the tasks as normal
await webHost.StartAsync(cancellationToken).ConfigureAwait(false);
}
public static string PrettyPrint(this TimeSpan expiration)
{
StringBuilder builder = new StringBuilder();
@ -185,7 +203,7 @@ namespace BTCPayServer
}
if(IPAddress.TryParse(server, out var ip))
{
return ip.IsLocal();
return ip.IsLocal() || ip.IsRFC1918();
}
return false;
}
@ -333,10 +351,15 @@ namespace BTCPayServer
NBitcoin.Extensions.TryAdd(ctx.Items, "BitpayAuth", value);
}
public static (string Signature, String Id, String Authorization) GetBitpayAuth(this HttpContext ctx)
public static bool TryGetBitpayAuth(this HttpContext ctx, out (string Signature, String Id, String Authorization) result)
{
ctx.Items.TryGetValue("BitpayAuth", out object obj);
return ((string Signature, String Id, String Authorization))obj;
if (ctx.Items.TryGetValue("BitpayAuth", out object obj))
{
result = ((string Signature, String Id, String Authorization))obj;
return true;
}
result = default;
return false;
}
public static StoreData GetStoreData(this HttpContext ctx)

@ -10,28 +10,35 @@ namespace BTCPayServer
{
public static class OpenIddictExtensions
{
public static OpenIddictServerBuilder ConfigureSigningKey(this OpenIddictServerBuilder builder,
IConfiguration configuration)
private static SecurityKey _key = null;
public static SecurityKey GetSigningKey(IConfiguration configuration)
{
if (_key != null)
{
return _key;
}
var file = Path.Combine(configuration.GetDataDir(), "rsaparams");
RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(2048);
RsaSecurityKey key = null;
if (File.Exists(file))
{
RSA.FromXmlString2( File.ReadAllText(file));
RSA.FromXmlString2(File.ReadAllText(file));
}
else
{
var contents = RSA.ToXmlString2(true);
File.WriteAllText(file,contents );
File.WriteAllText(file, contents);
}
RSAParameters KeyParam = RSA.ExportParameters(true);
key = new RsaSecurityKey(KeyParam);
return builder.AddSigningKey(key);
_key = new RsaSecurityKey(KeyParam);
return _key;
}
public static OpenIddictServerBuilder ConfigureSigningKey(this OpenIddictServerBuilder builder,
IConfiguration configuration)
{
return builder.AddSigningKey(GetSigningKey(configuration));
}
}
}

@ -52,6 +52,8 @@ namespace BTCPayServer.HostedServices
public AppType? RootAppType { get; set; }
public string RootAppId { get; set; }
public List<PoliciesSettings.DomainToAppMappingItem> DomainToAppMapping { get; set; }
internal void Update(PoliciesSettings data)
{
ShowRegister = !data.LockSubscription;
@ -59,6 +61,7 @@ namespace BTCPayServer.HostedServices
RootAppType = data.RootAppType;
RootAppId = data.RootAppId;
DomainToAppMapping = data.DomainToAppMapping;
}
}

@ -36,18 +36,15 @@ namespace BTCPayServer.HostedServices
InvoiceRepository _InvoiceRepository;
EventAggregator _EventAggregator;
BTCPayNetworkProvider _NetworkProvider;
ExplorerClientProvider _ExplorerClientProvider;
public InvoiceWatcher(
BTCPayNetworkProvider networkProvider,
InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
ExplorerClientProvider explorerClientProvider)
{
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_NetworkProvider = networkProvider;
_ExplorerClientProvider = explorerClientProvider;
}
CompositeDisposable leases = new CompositeDisposable();
@ -69,10 +66,9 @@ namespace BTCPayServer.HostedServices
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
var allPaymentMethods = invoice.GetPaymentMethods();
var paymentMethod = GetNearestClearedPayment(allPaymentMethods, out var accounting, _NetworkProvider);
var paymentMethod = GetNearestClearedPayment(allPaymentMethods, out var accounting);
if (paymentMethod == null)
return;
var network = _NetworkProvider.GetNetwork<BTCPayNetworkBase>(paymentMethod.GetId().CryptoCode);
if (invoice.Status == InvoiceStatus.New || invoice.Status == InvoiceStatus.Expired)
{
if (accounting.Paid >= accounting.MinimumTotalDue)
@ -125,7 +121,7 @@ namespace BTCPayServer.HostedServices
if (invoice.Status == InvoiceStatus.Paid)
{
var confirmedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy, network));
var confirmedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy));
if (// Is after the monitoring deadline
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
@ -149,7 +145,7 @@ namespace BTCPayServer.HostedServices
if (invoice.Status == InvoiceStatus.Confirmed)
{
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p));
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
{
context.Events.Add(new InvoiceEvent(invoice, 1006, InvoiceEvent.Completed));
@ -160,15 +156,13 @@ namespace BTCPayServer.HostedServices
}
public static PaymentMethod GetNearestClearedPayment(PaymentMethodDictionary allPaymentMethods, out PaymentMethodAccounting accounting, BTCPayNetworkProvider networkProvider)
public static PaymentMethod GetNearestClearedPayment(PaymentMethodDictionary allPaymentMethods, out PaymentMethodAccounting accounting)
{
PaymentMethod result = null;
accounting = null;
decimal nearestToZero = 0.0m;
foreach (var paymentMethod in allPaymentMethods)
{
if (networkProvider != null && networkProvider.GetNetwork<BTCPayNetworkBase>(paymentMethod.GetId().CryptoCode) == null)
continue;
var currentAccounting = paymentMethod.Calculate();
var distanceFromZero = Math.Abs(currentAccounting.DueUncapped.ToDecimal(MoneyUnit.BTC));
if (result == null || distanceFromZero < nearestToZero)
@ -318,12 +312,11 @@ namespace BTCPayServer.HostedServices
.GetPayments()
.Select<PaymentEntity, Task<PaymentEntity>>(async payment =>
{
var paymentNetwork = _NetworkProvider.GetNetwork<BTCPayNetwork>(payment.GetCryptoCode());
var paymentData = payment.GetCryptoPaymentData();
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
{
// Do update if confirmation count in the paymentData is not up to date
if ((onChainPaymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
if ((onChainPaymentData.ConfirmationCount < payment.Network.MaxTrackedConfirmation && payment.Accounted)
&& (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
{
var transactionResult = await _ExplorerClientProvider.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash);
@ -332,7 +325,7 @@ namespace BTCPayServer.HostedServices
payment.SetCryptoPaymentData(onChainPaymentData);
// we want to extend invoice monitoring until we reach max confirmations on all onchain payment methods
if (confirmationCount < paymentNetwork.MaxTrackedConfirmation)
if (confirmationCount < payment.Network.MaxTrackedConfirmation)
extendInvoiceMonitoring = true;
return payment;

@ -22,14 +22,12 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using BTCPayServer.Controllers;
using BTCPayServer.Services.Mails;
using Microsoft.AspNetCore.Identity;
using System.Threading;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Authentication;
using BTCPayServer.Authentication.OpenId.Models;
using BTCPayServer.Logging;
using BTCPayServer.HostedServices;
using System.Security.Claims;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
@ -37,16 +35,25 @@ using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services.PaymentRequests;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBXplorer.DerivationStrategy;
using NicolasDorier.RateLimits;
using Npgsql;
using BTCPayServer.Services.Apps;
using OpenIddict.EntityFrameworkCore.Models;
using BTCPayServer.Services.U2F;
using BundlerMinifier.TagHelpers;
using OpenIddict.EntityFrameworkCore.Models;
using System.Collections.Generic;
using System.Diagnostics;
using System.Security.Claims;
using System.Threading.Tasks;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Routing;
namespace BTCPayServer.Hosting
{
public static class BTCPayServerServices
@ -60,6 +67,10 @@ namespace BTCPayServer.Hosting
o.UseOpenIddict<BTCPayOpenIdClient, BTCPayOpenIdAuthorization, OpenIddictScope<string>, BTCPayOpenIdToken, string>();
});
services.AddHttpClient();
services.AddHttpClient(nameof(ExplorerClientProvider), httpClient =>
{
httpClient.Timeout = Timeout.InfiniteTimeSpan;
});
services.TryAddSingleton<SettingsRepository>();
services.TryAddSingleton<TorServices>();
services.TryAddSingleton<SocketFactory>();
@ -67,6 +78,7 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<InvoicePaymentNotification>();
services.TryAddSingleton<BTCPayServerOptions>(o =>
o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
services.AddStartupTask<MigrationStartupTask>();
services.TryAddSingleton<InvoiceRepository>(o =>
{
var opts = o.GetRequiredService<BTCPayServerOptions>();
@ -173,7 +185,6 @@ namespace BTCPayServer.Hosting
o.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(DerivationStrategyBase)));
});
services.AddSingleton<IHostedService, CssThemeManagerHostedService>();
services.AddSingleton<IHostedService, MigratorHostedService>();
services.AddSingleton<IHostedService, HostedServices.CheckConfigurationHostedService>();
@ -243,8 +254,9 @@ namespace BTCPayServer.Hosting
services.AddSingleton(rateLimits);
return services;
}
private static void AddBtcPayServerAuthenticationSchemes(this IServiceCollection services, IConfiguration configuration)
private static void AddBtcPayServerAuthenticationSchemes(this IServiceCollection services,
IConfiguration configuration)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();
@ -252,9 +264,51 @@ namespace BTCPayServer.Hosting
services.AddAuthentication()
.AddJwtBearer(options =>
{
// options.RequireHttpsMetadata = false;
// options.TokenValidationParameters.ValidateAudience = false;
//Disabled so that Tor works witt JWT auth
options.RequireHttpsMetadata = false;
options.TokenValidationParameters.ValidateAudience = false;
//we do not validate the issuer directly because btcpay can be accessed through multiple urls that we cannot predetermine
options.TokenValidationParameters.ValidateIssuer = false;
options.TokenValidationParameters.IssuerSigningKey =
OpenIddictExtensions.GetSigningKey(configuration);
options.IncludeErrorDetails = true;
options.Events = new JwtBearerEvents()
{
OnTokenValidated = async context =>
{
var routeData = context.HttpContext.GetRouteData();
var identity = ((ClaimsIdentity)context.Principal.Identity);
if (context.Principal.IsInRole(Roles.ServerAdmin))
{
identity.AddClaim(new Claim(Policies.CanModifyServerSettings.Key, "true"));
}
if (context.HttpContext.GetStoreData() != null ||
!routeData.Values.TryGetValue("storeId", out var storeId))
{
return;
}
var userManager = context.HttpContext.RequestServices
.GetService<UserManager<ApplicationUser>>();
var storeRepository = context.HttpContext.RequestServices
.GetService<StoreRepository>();
var userid = userManager.GetUserId(context.Principal);
if (!string.IsNullOrEmpty(userid))
{
var store = await storeRepository.FindStore((string)storeId, userid);
if (store == null)
{
context.Fail("Could not authorize you against store access");
}
else
{
context.HttpContext.SetStoreData(store);
identity.AddClaims(store.GetClaims());
}
}
}
};
})
.AddCookie()
.AddBitpayAuthentication();
@ -262,37 +316,9 @@ namespace BTCPayServer.Hosting
public static IApplicationBuilder UsePayServer(this IApplicationBuilder app)
{
using (var scope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
{
//Wait the DB is ready
Retry(() =>
{
scope.ServiceProvider.GetRequiredService<ApplicationDbContext>().Database.Migrate();
});
}
app.UseMiddleware<BTCPayMiddleware>();
return app;
}
static void Retry(Action act)
{
CancellationTokenSource cts = new CancellationTokenSource(1000);
while (true)
{
try
{
act();
return;
}
// Starting up
catch (PostgresException ex) when (ex.SqlState == "57P03") { Thread.Sleep(1000); }
catch when (!cts.IsCancellationRequested)
{
Thread.Sleep(100);
}
}
}
}

@ -20,6 +20,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core;
using OpenIddict.Abstractions;
using OpenIddict.EntityFrameworkCore.Models;
using System.Net;
using BTCPayServer.Authentication.OpenId;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Services.Apps;
using BTCPayServer.Storage;
@ -94,6 +95,10 @@ namespace BTCPayServer.Hosting
string httpsCertificateFilePath = Configuration.GetOrDefault<string>("HttpsCertificateFilePath", null);
bool useDefaultCertificate = Configuration.GetOrDefault<bool>("HttpsUseDefaultCertificate", false);
bool hasCertPath = !String.IsNullOrEmpty(httpsCertificateFilePath);
services.Configure<KestrelServerOptions>(kestrel =>
{
kestrel.Limits.MaxRequestLineSize = 8_192 * 10 * 5; // Around 500K, transactions passed in URI should not be bigger than this
});
if (hasCertPath || useDefaultCertificate)
{
var bindAddress = Configuration.GetOrDefault<IPAddress>("bind", IPAddress.Any);
@ -142,6 +147,9 @@ namespace BTCPayServer.Hosting
})
.AddServer(options =>
{
//Disabled so that Tor works with OpenIddict too
options.DisableHttpsRequirement();
// Register the ASP.NET Core MVC binder used by OpenIddict.
// Note: if you don't call this method, you won't be able to
// bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
@ -150,8 +158,11 @@ namespace BTCPayServer.Hosting
// Enable the token endpoint (required to use the password flow).
options.EnableTokenEndpoint("/connect/token");
options.EnableAuthorizationEndpoint("/connect/authorize");
options.EnableAuthorizationEndpoint("/connect/logout");
options.EnableLogoutEndpoint("/connect/logout");
//we do not care about these granular controls for now
options.DisableScopeValidation();
options.IgnoreEndpointPermissions();
// Allow client applications various flows
options.AllowImplicitFlow();
options.AllowClientCredentialsFlow();
@ -167,6 +178,12 @@ namespace BTCPayServer.Hosting
OpenIdConnectConstants.Scopes.Email,
OpenIdConnectConstants.Scopes.Profile,
OpenIddictConstants.Scopes.Roles);
options.AddEventHandler<PasswordGrantTypeEventHandler>();
options.AddEventHandler<AuthorizationCodeGrantTypeEventHandler>();
options.AddEventHandler<RefreshTokenGrantTypeEventHandler>();
options.AddEventHandler<ClientCredentialsGrantTypeEventHandler>();
options.AddEventHandler<AuthorizationEventHandler>();
options.AddEventHandler<LogoutEventHandler>();
options.ConfigureSigningKey(Configuration);
});

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer
{
public interface IStartupTask
{
Task ExecuteAsync(CancellationToken cancellationToken = default);
}
}

@ -8,16 +8,18 @@ using BTCPayServer.Data;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using BTCPayServer.Logging;
using System.Threading;
using Npgsql;
namespace BTCPayServer.HostedServices
namespace BTCPayServer
{
public class MigratorHostedService : BaseAsyncService
public class MigrationStartupTask : IStartupTask
{
private ApplicationDbContextFactory _DBContextFactory;
private StoreRepository _StoreRepository;
private BTCPayNetworkProvider _NetworkProvider;
private SettingsRepository _Settings;
public MigratorHostedService(
public MigrationStartupTask(
BTCPayNetworkProvider networkProvider,
StoreRepository storeRepository,
ApplicationDbContextFactory dbContextFactory,
@ -28,18 +30,11 @@ namespace BTCPayServer.HostedServices
_NetworkProvider = networkProvider;
_Settings = settingsRepository;
}
internal override Task[] InitializeTasks()
{
return new[]
{
Update()
};
}
private async Task Update()
public async Task ExecuteAsync(CancellationToken cancellationToken = default)
{
try
{
await Migrate(cancellationToken);
var settings = (await _Settings.GetSettingAsync<MigrationSettings>()) ?? new MigrationSettings();
if (!settings.DeprecatedLightningConnectionStringCheck)
{
@ -80,11 +75,34 @@ namespace BTCPayServer.HostedServices
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, "Error on the MigratorHostedService");
Logs.PayServer.LogError(ex, "Error on the MigrationStartupTask");
throw;
}
}
private async Task Migrate(CancellationToken cancellationToken)
{
using (CancellationTokenSource timeout = new CancellationTokenSource(10_000))
using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken))
{
retry:
try
{
await _DBContextFactory.CreateContext().Database.MigrateAsync();
}
// Starting up
catch when (!cts.Token.IsCancellationRequested)
{
try
{
await Task.Delay(1000, cts.Token);
}
catch { }
goto retry;
}
}
}
private async Task ConvertConvertWalletKeyPathRoots()
{
bool save = false;

@ -0,0 +1,850 @@
// <auto-generated />
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20190701082105_sort_paymentrequests")]
partial class sort_paymentrequests
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.11-servicing-32099");
modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdAuthorization", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ApplicationId");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50);
b.Property<string>("Properties");
b.Property<string>("Scopes");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(25);
b.Property<string>("Subject")
.IsRequired()
.HasMaxLength(450);
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(25);
b.HasKey("Id");
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
b.ToTable("OpenIddictAuthorizations");
});
modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdClient", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ApplicationUserId");
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(100);
b.Property<string>("ClientSecret");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50);
b.Property<string>("ConsentType");
b.Property<string>("DisplayName");
b.Property<string>("Permissions");
b.Property<string>("PostLogoutRedirectUris");
b.Property<string>("Properties");
b.Property<string>("RedirectUris");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(25);
b.HasKey("Id");
b.HasIndex("ApplicationUserId");
b.HasIndex("ClientId")
.IsUnique();
b.ToTable("OpenIddictApplications");
});
modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdToken", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ApplicationId");
b.Property<string>("AuthorizationId");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50);
b.Property<DateTimeOffset?>("CreationDate");
b.Property<DateTimeOffset?>("ExpirationDate");
b.Property<string>("Payload");
b.Property<string>("Properties");
b.Property<string>("ReferenceId")
.HasMaxLength(100);
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(25);
b.Property<string>("Subject")
.IsRequired()
.HasMaxLength(450);
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(25);
b.HasKey("Id");
b.HasIndex("AuthorizationId");
b.HasIndex("ReferenceId")
.IsUnique();
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
b.ToTable("OpenIddictTokens");
});
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.Property<bool>("TagAllInvoices");
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>("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");
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("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<DateTimeOffset>("Created")
.ValueGeneratedOnAdd()
.HasDefaultValue(new DateTimeOffset(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
b.Property<int>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("Status");
b.HasIndex("StoreDataId");
b.ToTable("PaymentRequests");
});
modelBuilder.Entity("BTCPayServer.Services.U2F.Models.U2FDevice", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ApplicationUserId");
b.Property<byte[]>("AttestationCert")
.IsRequired();
b.Property<int>("Counter");
b.Property<byte[]>("KeyHandle")
.IsRequired();
b.Property<string>("Name");
b.Property<byte[]>("PublicKey")
.IsRequired();
b.HasKey("Id");
b.HasIndex("ApplicationUserId");
b.ToTable("U2FDevices");
});
modelBuilder.Entity("BTCPayServer.Storage.Models.StoredFile", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ApplicationUserId");
b.Property<string>("FileName");
b.Property<string>("StorageFileName");
b.Property<DateTime>("Timestamp");
b.HasKey("Id");
b.HasIndex("ApplicationUserId");
b.ToTable("Files");
});
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("OpenIddict.EntityFrameworkCore.Models.OpenIddictScope<string>", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50);
b.Property<string>("Description");
b.Property<string>("DisplayName");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200);
b.Property<string>("Properties");
b.Property<string>("Resources");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("OpenIddictScopes");
});
modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdAuthorization", b =>
{
b.HasOne("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdClient", "Application")
.WithMany("Authorizations")
.HasForeignKey("ApplicationId");
});
modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdClient", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
.WithMany("OpenIdClients")
.HasForeignKey("ApplicationUserId");
});
modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdToken", b =>
{
b.HasOne("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdClient", "Application")
.WithMany("Tokens")
.HasForeignKey("ApplicationId");
b.HasOne("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdAuthorization", "Authorization")
.WithMany("Tokens")
.HasForeignKey("AuthorizationId");
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("APIKeys")
.HasForeignKey("StoreId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("Apps")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("HistoricalAddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("Invoices")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Events")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("PairedSINs")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Payments")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("PendingInvoices")
.HasForeignKey("Id")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("RefundAddresses")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
.WithMany("UserStores")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("UserStores")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("PaymentRequests")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Services.U2F.Models.U2FDevice", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
.WithMany("U2FDevices")
.HasForeignKey("ApplicationUserId");
});
modelBuilder.Entity("BTCPayServer.Storage.Models.StoredFile", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
.WithMany("StoredFiles")
.HasForeignKey("ApplicationUserId");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<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
}
}
}

@ -0,0 +1,24 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace BTCPayServer.Migrations
{
public partial class sort_paymentrequests : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTimeOffset>(
name: "Created",
table: "PaymentRequests",
nullable: false,
defaultValue: new DateTimeOffset(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Created",
table: "PaymentRequests");
}
}
}

@ -14,7 +14,7 @@ namespace BTCPayServer.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.8-servicing-32085");
.HasAnnotation("ProductVersion", "2.1.11-servicing-32099");
modelBuilder.Entity("BTCPayServer.Authentication.OpenId.Models.BTCPayOpenIdAuthorization", b =>
{
@ -458,6 +458,10 @@ namespace BTCPayServer.Migrations
b.Property<byte[]>("Blob");
b.Property<DateTimeOffset>("Created")
.ValueGeneratedOnAdd()
.HasDefaultValue(new DateTimeOffset(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
b.Property<int>("Status");
b.Property<string>("StoreDataId");

@ -30,6 +30,7 @@ namespace BTCPayServer.Models.StoreViewModels
public string NotifyEmail { get; set; }
public string StoreId { get; set; }
public string CheckoutQueryString { get; set; }
// Data that influences Pay Button UI, but not invoice creation
public string UrlRoot { get; set; }

@ -27,6 +27,8 @@ namespace BTCPayServer.Payments.Bitcoin
RBF = rbf;
}
[JsonIgnore]
public BTCPayNetworkBase Network { get; set; }
[JsonIgnore]
public OutPoint Outpoint { get; set; }
[JsonIgnore]
public TxOut Output { get; set; }
@ -54,12 +56,12 @@ namespace BTCPayServer.Payments.Bitcoin
return Output.Value.ToDecimal(MoneyUnit.BTC);
}
public bool PaymentCompleted(PaymentEntity entity, BTCPayNetworkBase network)
public bool PaymentCompleted(PaymentEntity entity)
{
return ConfirmationCount >= network.MaxTrackedConfirmation;
return ConfirmationCount >= Network.MaxTrackedConfirmation;
}
public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetworkBase network)
public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy)
{
if (speedPolicy == SpeedPolicy.HighSpeed)
{
@ -80,14 +82,14 @@ namespace BTCPayServer.Payments.Bitcoin
return false;
}
public BitcoinAddress GetDestination(BTCPayNetworkBase network)
public BitcoinAddress GetDestination()
{
return Output.ScriptPubKey.GetDestinationAddress(((BTCPayNetwork)network).NBitcoinNetwork);
return Output.ScriptPubKey.GetDestinationAddress(((BTCPayNetwork)Network).NBitcoinNetwork);
}
string CryptoPaymentData.GetDestination(BTCPayNetworkBase network)
string CryptoPaymentData.GetDestination()
{
return GetDestination(network).ToString();
return GetDestination().ToString();
}
}
}

@ -361,7 +361,7 @@ namespace BTCPayServer.Payments.Bitcoin
invoice = (await UpdatePaymentStates(wallet, invoice.Id));
if (invoice == null)
return null;
var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike, _ExplorerClients.NetworkProviders);
var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike);
if (paymentMethod != null &&
paymentMethod.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod btc &&
btc.GetDepositAddress(wallet.Network.NBitcoinNetwork).ScriptPubKey == paymentData.Output.ScriptPubKey &&

@ -14,13 +14,15 @@ namespace BTCPayServer.Payments.Lightning
{
public class LightningLikePaymentData : CryptoPaymentData
{
[JsonIgnore]
public BTCPayNetworkBase Network { get; set; }
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney Amount { get; set; }
public string BOLT11 { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
public uint256 PaymentHash { get; set; }
public string GetDestination(BTCPayNetworkBase network)
public string GetDestination()
{
return BOLT11;
}
@ -49,12 +51,12 @@ namespace BTCPayServer.Payments.Lightning
return Amount.ToDecimal(LightMoneyUnit.BTC);
}
public bool PaymentCompleted(PaymentEntity entity, BTCPayNetworkBase network)
public bool PaymentCompleted(PaymentEntity entity)
{
return true;
}
public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetworkBase network)
public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy)
{
return true;
}

@ -53,6 +53,7 @@ namespace BTCPayServer
l.AddFilter("Microsoft", LogLevel.Error);
l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical);
l.AddFilter("Microsoft.AspNetCore.Antiforgery.Internal", LogLevel.Critical);
l.AddFilter("AspNet.Security.OpenIdConnect.Server.OpenIdConnectServerHandler", LogLevel.Error);
l.AddProvider(new CustomConsoleLogProvider(processor));
// Use Serilog for debug log file.
@ -68,7 +69,7 @@ namespace BTCPayServer
})
.UseStartup<Startup>()
.Build();
host.StartAsync().GetAwaiter().GetResult();
host.StartWithTasksAsync().GetAwaiter().GetResult();
var urls = host.ServerFeatures.Get<IServerAddressesFeature>().Addresses;
foreach (var url in urls)
{

@ -0,0 +1,15 @@
using System;
using BTCPayServer.Security.Bitpay;
using Microsoft.AspNetCore.Authentication;
namespace BTCPayServer.Security
{
public static class AuthenticationExtensions
{
public static AuthenticationBuilder AddBitpayAuthentication(this AuthenticationBuilder builder)
{
builder.AddScheme<BitpayAuthenticationOptions, BitpayAuthenticationHandler>(Policies.BitpayAuthentication, o => { });
return builder;
}
}
}

@ -1,4 +1,4 @@
using System.Security.Claims;
using System.Security.Claims;
using System.Threading.Tasks;
using BTCPayServer.Models;
using BTCPayServer.Services.Stores;
@ -29,12 +29,9 @@ namespace BTCPayServer.Security
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var principal = context.HttpContext.User;
if (context.HttpContext.GetIsBitpayAPI())
{
if (context.HttpContext.User?.Identity?.AuthenticationType != Policies.CookieAuthentication)
return;
}
var principal = context.HttpContext.User;
var identity = ((ClaimsIdentity)principal.Identity);
if (principal.IsInRole(Roles.ServerAdmin))
{
@ -55,10 +52,7 @@ namespace BTCPayServer.Security
else
{
context.HttpContext.SetStoreData(store);
if (store != null)
{
identity.AddClaims(store.GetClaims());
}
identity.AddClaims(store.GetClaims());
}
}
}

@ -0,0 +1,173 @@
using System;
using Microsoft.Extensions.Logging;
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.Services;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Http;
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;
using Microsoft.AspNetCore.Authentication;
using System.Text.Encodings.Web;
namespace BTCPayServer.Security.Bitpay
{
public class BitpayAuthenticationHandler : AuthenticationHandler<BitpayAuthenticationOptions>
{
StoreRepository _StoreRepository;
TokenRepository _TokenRepository;
public BitpayAuthenticationHandler(
TokenRepository tokenRepository,
StoreRepository storeRepository,
IOptionsMonitor<BitpayAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
_TokenRepository = tokenRepository;
_StoreRepository = storeRepository;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
List<Claim> claims = new List<Claim>();
if (!Context.Request.HttpContext.TryGetBitpayAuth(out var bitpayAuth))
return AuthenticateResult.NoResult();
string storeId = null;
bool anonymous = true;
bool? success = null;
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
{
var result = await CheckBitId(Context.Request.HttpContext, bitpayAuth.Signature, bitpayAuth.Id, claims);
storeId = result.StoreId;
success = result.SuccessAuth;
anonymous = false;
}
else if (!string.IsNullOrEmpty(bitpayAuth.Authorization))
{
storeId = await CheckLegacyAPIKey(Context.Request.HttpContext, bitpayAuth.Authorization);
success = storeId != null;
anonymous = false;
}
else
{
if (Context.Request.HttpContext.Request.Query.TryGetValue("storeId", out var storeIdStringValues))
{
storeId = storeIdStringValues.FirstOrDefault() ?? string.Empty;
success = true;
anonymous = true;
}
}
if (success is true)
{
if (storeId != null)
{
claims.Add(new Claim(Policies.CanCreateInvoice.Key, storeId));
var store = await _StoreRepository.FindStore(storeId);
if (store == null ||
(anonymous && !store.GetStoreBlob().AnyoneCanInvoice))
{
return AuthenticateResult.Fail("Invalid credentials");
}
store.AdditionalClaims.AddRange(claims);
Context.Request.HttpContext.SetStoreData(store);
}
return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(claims, Policies.BitpayAuthentication)), Policies.BitpayAuthentication));
}
else if (success is false)
{
return AuthenticateResult.Fail("Invalid credentials");
}
return AuthenticateResult.NoResult();
}
private async Task<(string StoreId, bool SuccessAuth)> CheckBitId(HttpContext httpContext, string sig, string id, List<Claim> claims)
{
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();
claims.Add(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", StringComparison.OrdinalIgnoreCase)?.Value?.Value<string>();
}
catch { }
}
if (token != null)
{
var bitToken = (await _TokenRepository.GetTokens(sin)).FirstOrDefault();
if (bitToken == null)
{
return (null, false);
}
storeId = bitToken.StoreId;
}
}
else
{
return (storeId, false);
}
}
catch (FormatException) { }
return (storeId, true);
}
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);
}
}
}

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
namespace BTCPayServer.Security.Bitpay
{
public class BitpayAuthenticationOptions : AuthenticationSchemeOptions
{
}
}

@ -1,187 +0,0 @@
using System;
using Microsoft.Extensions.Logging;
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.Services;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Http;
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;
using Microsoft.AspNetCore.Authentication;
using System.Text.Encodings.Web;
namespace BTCPayServer.Security
{
public class BitpayAuthentication
{
public class BitpayAuthOptions : AuthenticationSchemeOptions
{
}
class BitpayAuthHandler : AuthenticationHandler<BitpayAuthOptions>
{
StoreRepository _StoreRepository;
TokenRepository _TokenRepository;
public BitpayAuthHandler(
TokenRepository tokenRepository,
StoreRepository storeRepository,
IOptionsMonitor<BitpayAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
_TokenRepository = tokenRepository;
_StoreRepository = storeRepository;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (Context.Request.HttpContext.GetIsBitpayAPI())
{
List<Claim> claims = new List<Claim>();
var bitpayAuth = Context.Request.HttpContext.GetBitpayAuth();
string storeId = null;
bool anonymous = true;
bool? success = null;
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
{
var result = await CheckBitId(Context.Request.HttpContext, bitpayAuth.Signature, bitpayAuth.Id, claims);
storeId = result.StoreId;
success = result.SuccessAuth;
anonymous = false;
}
else if (!string.IsNullOrEmpty(bitpayAuth.Authorization))
{
storeId = await CheckLegacyAPIKey(Context.Request.HttpContext, bitpayAuth.Authorization);
success = storeId != null;
anonymous = false;
}
else
{
if (Context.Request.HttpContext.Request.Query.TryGetValue("storeId", out var storeIdStringValues))
{
storeId = storeIdStringValues.FirstOrDefault() ?? string.Empty;
success = true;
anonymous = true;
}
}
if (success is true)
{
if (storeId != null)
{
claims.Add(new Claim(Policies.CanCreateInvoice.Key, storeId));
var store = await _StoreRepository.FindStore(storeId);
if (store == null ||
(anonymous && !store.GetStoreBlob().AnyoneCanInvoice))
{
return AuthenticateResult.Fail("Invalid credentials");
}
store.AdditionalClaims.AddRange(claims);
Context.Request.HttpContext.SetStoreData(store);
}
return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(claims, Policies.BitpayAuthentication)), Policies.BitpayAuthentication));
}
else if (success is false)
{
return AuthenticateResult.Fail("Invalid credentials");
}
// else if (success is null)
}
return AuthenticateResult.NoResult();
}
private async Task<(string StoreId, bool SuccessAuth)> CheckBitId(HttpContext httpContext, string sig, string id, List<Claim> claims)
{
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();
claims.Add(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", StringComparison.OrdinalIgnoreCase)?.Value?.Value<string>();
}
catch { }
}
if (token != null)
{
var bitToken = (await _TokenRepository.GetTokens(sin)).FirstOrDefault();
if (bitToken == null)
{
return (null, false);
}
storeId = bitToken.StoreId;
}
}
else
{
return (storeId, false);
}
}
catch (FormatException) { }
return (storeId, true);
}
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);
}
}
public static void AddAuthentication(AuthenticationBuilder builder, Action<BitpayAuthOptions> bitpayAuth = null)
{
bitpayAuth = bitpayAuth ?? new Action<BitpayAuthOptions>((o) => { });
builder.AddScheme<BitpayAuthOptions, BitpayAuthHandler>(Policies.BitpayAuthentication, bitpayAuth);
}
}
}

@ -1,15 +0,0 @@
using System;
using Microsoft.AspNetCore.Authentication;
namespace BTCPayServer.Security
{
public static class BitpayAuthenticationExtensions
{
public static AuthenticationBuilder AddBitpayAuthentication(this AuthenticationBuilder builder,
Action<BitpayAuthentication.BitpayAuthOptions> bitpayAuth = null)
{
BitpayAuthentication.AddAuthentication(builder,bitpayAuth);
return builder;
}
}
}

@ -56,8 +56,7 @@ namespace BTCPayServer.Services.Apps
{
data.GetValue(),
invoiceEvent.Payment.GetCryptoCode(),
Enum.GetName(typeof(PaymentTypes),
invoiceEvent.Payment.GetPaymentMethodId().PaymentType)
invoiceEvent.Payment.GetPaymentMethodId().PaymentType.ToString()
}, cancellationToken);
}
await InfoUpdated(appId);

@ -37,11 +37,9 @@ namespace BTCPayServer.Services.Apps
CurrencyNameTable _Currencies;
private readonly StoreRepository _storeRepository;
private readonly HtmlSanitizer _HtmlSanitizer;
private readonly BTCPayNetworkProvider _Networks;
public CurrencyNameTable Currencies => _Currencies;
public AppService(ApplicationDbContextFactory contextFactory,
InvoiceRepository invoiceRepository,
BTCPayNetworkProvider networks,
CurrencyNameTable currencies,
StoreRepository storeRepository,
HtmlSanitizer htmlSanitizer)
@ -51,7 +49,6 @@ namespace BTCPayServer.Services.Apps
_Currencies = currencies;
_storeRepository = storeRepository;
_HtmlSanitizer = htmlSanitizer;
_Networks = networks;
}
public async Task<object> GetAppInfo(string appId)
@ -325,7 +322,7 @@ namespace BTCPayServer.Services.Apps
var paymentMethodContribution = new Contribution();
paymentMethodContribution.PaymentMehtodId = pay.GetPaymentMethodId();
paymentMethodContribution.Value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee;
var rate = p.GetPaymentMethod(paymentMethodContribution.PaymentMehtodId, _Networks).Rate;
var rate = p.GetPaymentMethod(paymentMethodContribution.PaymentMehtodId).Rate;
paymentMethodContribution.CurrencyValue = rate * paymentMethodContribution.Value;
return paymentMethodContribution;
})

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

@ -38,10 +38,10 @@ namespace BTCPayServer.Services
List<KeyPath> derivations = new List<KeyPath>();
if (network.NBitcoinNetwork.Consensus.SupportSegwit)
{
if (derivation.Redeem?.IsWitness is true ||
derivation.ScriptPubKey.IsWitness) // Native or p2sh segwit
if (derivation.Redeem?.IsScriptType(ScriptType.Witness) is true ||
derivation.ScriptPubKey.IsScriptType(ScriptType.Witness)) // Native or p2sh segwit
derivations.Add(new KeyPath("49'"));
if (derivation.Redeem == null && derivation.ScriptPubKey.IsWitness) // Native segwit
if (derivation.Redeem == null && derivation.ScriptPubKey.IsScriptType(ScriptType.Witness)) // Native segwit
derivations.Add(new KeyPath("84'"));
}
derivations.Add(new KeyPath("44'"));

@ -16,9 +16,8 @@ namespace BTCPayServer.Services.Invoices.Export
public BTCPayNetworkProvider Networks { get; }
public CurrencyNameTable Currencies { get; }
public InvoiceExport(BTCPayNetworkProvider networks, CurrencyNameTable currencies)
public InvoiceExport(CurrencyNameTable currencies)
{
Networks = networks;
Currencies = currencies;
}
public string Process(InvoiceEntity[] invoices, string fileFormat)
@ -68,7 +67,7 @@ namespace BTCPayServer.Services.Invoices.Export
var cryptoCode = payment.GetPaymentMethodId().CryptoCode;
var pdata = payment.GetCryptoPaymentData();
var pmethod = invoice.GetPaymentMethod(payment.GetPaymentMethodId(), Networks);
var pmethod = invoice.GetPaymentMethod(payment.GetPaymentMethodId());
var paidAfterNetworkFees = pdata.GetValue() - payment.NetworkFee;
invoiceDue -= paidAfterNetworkFees * pmethod.Rate;
@ -79,7 +78,7 @@ namespace BTCPayServer.Services.Invoices.Export
CryptoCode = cryptoCode,
ConversionRate = pmethod.Rate,
PaymentType = payment.GetPaymentMethodId().PaymentType.ToPrettyString(),
Destination = payment.GetCryptoPaymentData().GetDestination(Networks.GetNetwork<BTCPayNetworkBase>(cryptoCode)),
Destination = pdata.GetDestination(),
Paid = pdata.GetValue().ToString(CultureInfo.InvariantCulture),
PaidCurrency = Math.Round(pdata.GetValue() * pmethod.Rate, currency.NumberDecimalDigits).ToString(CultureInfo.InvariantCulture),
// Adding NetworkFee because Paid doesn't take into account network fees

@ -15,6 +15,7 @@ using NBXplorer.DerivationStrategy;
using BTCPayServer.Payments;
using NBitpayClient;
using BTCPayServer.Payments.Bitcoin;
using System.ComponentModel.DataAnnotations.Schema;
namespace BTCPayServer.Services.Invoices
{
@ -432,9 +433,9 @@ namespace BTCPayServer.Services.Invoices
Id = data.GetPaymentId(),
Fee = entity.NetworkFee,
Value = data.GetValue(),
Completed = data.PaymentCompleted(entity, info.Network),
Confirmed = data.PaymentConfirmed(entity, SpeedPolicy, info.Network),
Destination = data.GetDestination(info.Network),
Completed = data.PaymentCompleted(entity),
Confirmed = data.PaymentConfirmed(entity, SpeedPolicy),
Destination = data.GetDestination(),
PaymentType = data.GetPaymentType().ToString(),
ReceivedDate = entity.ReceivedTime.DateTime
};
@ -519,14 +520,14 @@ namespace BTCPayServer.Services.Invoices
return rates.TryGet(paymentMethodId) != null;
}
public PaymentMethod GetPaymentMethod(PaymentMethodId paymentMethodId, BTCPayNetworkProvider networkProvider)
public PaymentMethod GetPaymentMethod(PaymentMethodId paymentMethodId)
{
GetPaymentMethods().TryGetValue(paymentMethodId, out var data);
return data;
}
public PaymentMethod GetPaymentMethod(BTCPayNetworkBase network, PaymentType paymentType, BTCPayNetworkProvider networkProvider)
public PaymentMethod GetPaymentMethod(BTCPayNetworkBase network, PaymentType paymentType)
{
return GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentType), networkProvider);
return GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentType));
}
public PaymentMethodDictionary GetPaymentMethods()
@ -898,6 +899,9 @@ namespace BTCPayServer.Services.Invoices
public class PaymentEntity
{
[NotMapped]
[JsonIgnore]
public BTCPayNetwork Network { get; set; }
public int Version { get; set; }
public DateTimeOffset ReceivedTime
{
@ -943,6 +947,7 @@ namespace BTCPayServer.Services.Invoices
{
// For invoices created when CryptoPaymentDataType was not existing, we just consider that it is a RBFed payment for safety
var bitcoin = new BitcoinLikePaymentData();
bitcoin.Network = Network;
bitcoin.Outpoint = Outpoint;
bitcoin.Output = Output;
bitcoin.RBF = true;
@ -955,6 +960,7 @@ namespace BTCPayServer.Services.Invoices
else
{
paymentData = GetPaymentMethodId().PaymentType.DeserializePaymentData(CryptoPaymentData);
paymentData.Network = Network;
if (paymentData is BitcoinLikePaymentData bitcoin)
{
bitcoin.Output = Output;
@ -1014,6 +1020,8 @@ namespace BTCPayServer.Services.Invoices
/// </summary>
public interface CryptoPaymentData
{
[JsonIgnore]
BTCPayNetworkBase Network { get; set; }
/// <summary>
/// Returns an identifier which uniquely identify the payment
/// </summary>
@ -1030,10 +1038,10 @@ namespace BTCPayServer.Services.Invoices
/// </summary>
/// <returns>The amount paid</returns>
decimal GetValue();
bool PaymentCompleted(PaymentEntity entity, BTCPayNetworkBase network);
bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetworkBase network);
bool PaymentCompleted(PaymentEntity entity);
bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy);
PaymentType GetPaymentType();
string GetDestination(BTCPayNetworkBase network);
string GetDestination();
}
}

@ -248,7 +248,7 @@ retry:
return false;
var invoiceEntity = ToObject(invoice.Blob);
var currencyData = invoiceEntity.GetPaymentMethod(network, paymentMethod.GetPaymentType(), null);
var currencyData = invoiceEntity.GetPaymentMethod(network, paymentMethod.GetPaymentType());
if (currencyData == null)
return false;
@ -441,6 +441,7 @@ retry:
entity.Payments = invoice.Payments.Select(p =>
{
var paymentEntity = ToObject<PaymentEntity>(p.Blob, null);
paymentEntity.Network = _Networks.GetNetwork<BTCPayNetwork>(paymentEntity.CryptoCode);
paymentEntity.Accounted = p.Accounted;
// PaymentEntity on version 0 does not have their own fee, because it was assumed that the payment method have fixed fee.
// We want to hide this legacy detail in InvoiceRepository, so we fetch the fee from the PaymentMethod and assign it to the PaymentEntity.
@ -646,7 +647,7 @@ retry:
if (invoice == null)
return null;
InvoiceEntity invoiceEntity = ToObject(invoice.Blob);
PaymentMethod paymentMethod = invoiceEntity.GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentData.GetPaymentType()), null);
PaymentMethod paymentMethod = invoiceEntity.GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentData.GetPaymentType()));
IPaymentMethodDetails paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
PaymentEntity entity = new PaymentEntity
{
@ -656,7 +657,8 @@ retry:
#pragma warning restore CS0618
ReceivedTime = date.UtcDateTime,
Accounted = accounted,
NetworkFee = paymentMethodDetails.GetNextNetworkFee()
NetworkFee = paymentMethodDetails.GetNextNetworkFee(),
Network = network as BTCPayNetwork
};
entity.SetCryptoPaymentData(paymentData);

@ -21,6 +21,7 @@ namespace BTCPayServer.Services.Invoices
}
public IPaymentMethodHandler this[PaymentMethodId index] => _mappedHandlers[index];
public bool Support(PaymentMethodId paymentMethod) => _mappedHandlers.ContainsKey(paymentMethod);
public IEnumerator<IPaymentMethodHandler> GetEnumerator()
{
return _mappedHandlers.Values.GetEnumerator();

@ -116,7 +116,9 @@ namespace BTCPayServer.Services.PaymentRequests
}
var total = await queryable.CountAsync(cancellationToken);
queryable = queryable.OrderByDescending(u => u.Created);
if (query.Skip.HasValue)
{
queryable = queryable.Skip(query.Skip.Value);
@ -126,7 +128,6 @@ namespace BTCPayServer.Services.PaymentRequests
{
queryable = queryable.Take(query.Count.Value);
}
return (total, await queryable.ToArrayAsync(cancellationToken));
}
}
@ -207,6 +208,10 @@ namespace BTCPayServer.Services.PaymentRequests
public class PaymentRequestData
{
public string Id { get; set; }
public DateTimeOffset Created
{
get; set;
}
public string StoreDataId { get; set; }
public StoreData StoreData { get; set; }

@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Apps;
using BTCPayServer.Validation;
using Newtonsoft.Json;
namespace BTCPayServer.Services
@ -23,7 +24,16 @@ namespace BTCPayServer.Services
[Display(Name = "Display app on website root")]
public string RootAppId { get; set; }
public AppType? RootAppType { get; set; }
public List<DomainToAppMappingItem> DomainToAppMapping { get; set; } = new List<DomainToAppMappingItem>();
public class DomainToAppMappingItem
{
[Display(Name = "Domain")][Required][HostName] public string Domain { get; set; }
[Display(Name = "App")][Required] public string AppId { get; set; }
public AppType AppType { get; set; }
}
}
}

@ -99,7 +99,7 @@ namespace BTCPayServer.Services.Rates
AddCurrency(_CurrencyProviders, network.CryptoCode, 8, network.CryptoCode);
}
}
return _CurrencyProviders.TryGet(currency);
return _CurrencyProviders.TryGet(currency.ToUpperInvariant());
}
}

@ -72,7 +72,7 @@ namespace BTCPayServer.Storage.Services.Providers.FileSystemStorage
await File.WriteAllTextAsync(Path.Combine(GetTempStorageDir(_options), name), JsonConvert.SerializeObject(localFileDescriptor));
return new Uri(baseUri,$"{LocalStorageDirectoryName}tmp/{name}" ).AbsoluteUri;
return new Uri(baseUri,$"{LocalStorageDirectoryName}tmp/{name}{(isDownload ? "?download" : string.Empty)}").AbsoluteUri;
}
}
}

@ -7,6 +7,7 @@ using BTCPayServer.Storage.Services.Providers.AzureBlobStorage;
using BTCPayServer.Storage.Services.Providers.FileSystemStorage;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
@ -57,13 +58,7 @@ namespace BTCPayServer.Storage
ServeUnknownFileTypes = true,
RequestPath = new PathString($"/{FileSystemFileProviderService.LocalStorageDirectoryName}"),
FileProvider = new PhysicalFileProvider(dirInfo.FullName),
OnPrepareResponse = context =>
{
if (context.Context.Request.Query.ContainsKey("download"))
{
context.Context.Response.Headers["Content-Disposition"] = "attachment";
}
}
OnPrepareResponse = HandleStaticFileResponse()
});
builder.UseStaticFiles(new StaticFileOptions()
{
@ -71,13 +66,7 @@ namespace BTCPayServer.Storage
RequestPath = new PathString($"/{FileSystemFileProviderService.LocalStorageDirectoryName}tmp"),
FileProvider = new TemporaryLocalFileProvider(tmpdirInfo, dirInfo,
builder.ApplicationServices.GetService<StoredFileRepository>()),
OnPrepareResponse = context =>
{
if (context.Context.Request.Query.ContainsKey("download"))
{
context.Context.Response.Headers["Content-Disposition"] = "attachment";
}
}
OnPrepareResponse = HandleStaticFileResponse()
});
}
catch (Exception e)
@ -85,5 +74,16 @@ namespace BTCPayServer.Storage
Logs.Utils.LogError(e, $"Could not initialize the Local File Storage system(uploading and storing files locally)");
}
}
private static Action<StaticFileResponseContext> HandleStaticFileResponse()
{
return context =>
{
if (context.Context.Request.Query.ContainsKey("download"))
{
context.Context.Response.Headers["Content-Disposition"] = "attachment";
}
};
}
}
}

@ -0,0 +1,23 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
namespace BTCPayServer.Validation
{
//from http://stackoverflow.com/questions/967516/ddg#967610
public class HostNameAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var str = value == null ? null : Convert.ToString(value, CultureInfo.InvariantCulture);
var valid = string.IsNullOrWhiteSpace(str) || Uri.CheckHostName(str) != UriHostNameType.Unknown;
if (!valid)
{
return new ValidationResult(ErrorMessage);
}
return ValidationResult.Success;
}
}
}

@ -5,11 +5,14 @@
<section>
<div class="container">
@if (TempData.ContainsKey("StatusMessage"))
{
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
</div>
</div>
}
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">@ViewData["Title"]</h2>

@ -14,13 +14,14 @@
</div>
<div class="modal-body">
<form asp-route-returnurl="@ViewData["ReturnUrl"]" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<label for="Email" class="input-group-text"><span class="input-group-addon fa fa-user"></span></label>
</div>
<input asp-for="Email" class="form-control" placeholder="Email" required="required" />
<input asp-for="Email" class="form-control" placeholder="Email" required="required"/>
</div>
<span asp-validation-for="Email" class="text-danger"></span>
</div>
@ -29,7 +30,7 @@
<div class="input-group-prepend">
<label for="Password" class="input-group-text"><span class="input-group-addon fa fa-lock"></span></label>
</div>
<input asp-for="Password" class="form-control" placeholder="Password" required="required" />
<input asp-for="Password" class="form-control" placeholder="Password" required="required"/>
</div>
<span asp-validation-for="Password" class="text-danger"></span>
</div>

@ -1,4 +1,4 @@
@model LoginWith2faViewModel
@model LoginWith2faViewModel
<section>
<div class="container-fluid">
@ -9,7 +9,8 @@
</div>
</div>
<div class="row">
<form class="col-12" method="post" asp-route-returnUrl="@ViewData["ReturnUrl"]" asp-action="LoginWith2fa">
<form class="col-12" method="post" asp-route-returnUrl="@ViewData["ReturnUrl"]" asp-action="LoginWith2fa">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input asp-for="RememberMe" type="hidden"/>
<div class="form-group">
<label asp-for="TwoFactorCode"></label>

@ -12,12 +12,13 @@
</div>
<div class="modal-body">
<form asp-route-returnUrl="@ViewData["ReturnUrl"]" asp-route-logon="@ViewData["Logon"]" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<label for="Email" class="input-group-text"><span class="input-group-addon fa fa-user"></span></label>
</div>
<input asp-for="Email" class="form-control" placeholder="Email" required="required" />
<input asp-for="Email" class="form-control" placeholder="Email" required="required"/>
</div>
<span asp-validation-for="Email" class="text-danger"></span>
</div>
@ -26,7 +27,7 @@
<div class="input-group-prepend">
<label for="Password" class="input-group-text"><span class="input-group-addon fa fa-lock"></span></label>
</div>
<input asp-for="Password" class="form-control" placeholder="Password" required="required" />
<input asp-for="Password" class="form-control" placeholder="Password" required="required"/>
</div>
<span asp-validation-for="Password" class="text-danger"></span>
</div>
@ -35,7 +36,7 @@
<div class="input-group-prepend">
<label for="ConfirmPassword" class="input-group-text"><span class="input-group-addon fa fa-lock"></span></label>
</div>
<input asp-for="ConfirmPassword" class="form-control" placeholder="Repeat password" required="required" />
<input asp-for="ConfirmPassword" class="form-control" placeholder="Repeat password" required="required"/>
</div>
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
@ -43,7 +44,7 @@
{
<div class="form-group">
<label asp-for="IsAdmin"></label>
<input asp-for="IsAdmin" type="checkbox" class="form-check-inline" />
<input asp-for="IsAdmin" type="checkbox" class="form-check-inline"/>
<span asp-validation-for="IsAdmin" class="text-danger"></span>
</div>
}

@ -5,11 +5,14 @@
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
@if (TempData.ContainsKey("StatusMessage"))
{
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
</div>
</div>
</div>
}
<div class="row">
<div class="col-md-4">
<form method="post">

@ -3,16 +3,16 @@
@{
ViewData["Title"] = "Apps";
}
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
@if (TempData.ContainsKey("TempDataProperty-StatusMessage"))
{
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
</div>
</div>
</div>
}
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">@ViewData["Title"]</h2>

@ -30,11 +30,14 @@
<hr class="primary">
</div>
</div>
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
@if (TempData.ContainsKey("TempDataProperty-StatusMessage"))
{
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
</div>
</div>
</div>
}
<div class="row">
<div class="col-lg-12">
<form method="post">
@ -44,7 +47,6 @@
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Tagline" class="control-label"></label>
<input asp-for="Tagline" class="form-control" />
@ -82,7 +84,6 @@
<div class="form-group">
<label asp-for="ResetEvery" class="control-label"></label>
<div class="input-group">
<input type="number" asp-for="ResetEveryAmount" placeholder="Amount" class="form-control">
<select class="custom-select" asp-for="ResetEvery">
@foreach (var opt in Model.ResetEveryValues)
@ -142,7 +143,6 @@
<span asp-validation-for="NotificationUrl" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="NotificationEmail" class="control-label"></label>
@if (Model.NotificationEmailWarning)
{
@ -181,7 +181,6 @@
<input asp-for="SoundsEnabled" type="checkbox" class="form-check" />
<span asp-validation-for="SoundsEnabled" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Sounds"></label>
<textarea asp-for="Sounds" class="form-control"></textarea>
@ -192,7 +191,6 @@
<input asp-for="AnimationsEnabled" type="checkbox" class="form-check" />
<span asp-validation-for="AnimationsEnabled" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="AnimationColors"></label>
<textarea asp-for="AnimationColors" class="form-control"></textarea>
@ -208,7 +206,6 @@
<input asp-for="DisqusShortname" class="form-control" />
<span asp-validation-for="DisqusShortname" class="text-danger"></span>
</div>
<input type="hidden" asp-for="NotificationEmailWarning" />
<div class="form-group">
<input type="submit" class="btn btn-primary" value="Save Settings" id="SaveSettings" />
@ -216,19 +213,15 @@
<a class="btn btn-secondary" target="_blank" asp-action="ViewCrowdfund" asp-controller="AppsPublic" asp-route-appId="@Model.AppId" id="ViewApp">View App</a>
<a class="btn btn-secondary" target="_blank" asp-action="ListApps">Back to the app list</a>
</div>
</form>
</div>
</div>
</div>
</section>
@section Scripts {
<script src="~/vendor/moment/moment.js"></script>
<bundle name="wwwroot/bundles/crowdfund-admin-bundle.min.js"></bundle>
<bundle name="wwwroot/bundles/crowdfund-admin-bundle.min.css"></bundle>
<script id="template-product-item" type="text/template">
<div class="col-sm-4 col-md-3 mb-3">
<div class="card">
@ -241,7 +234,6 @@
</div>
</div>
</script>
<script id="template-product-content" type="text/template">
<div class="mb-3">
<input class="js-product-id" type="hidden" name="id" value="{id}">
@ -277,4 +269,3 @@
</div>
</script>
}

@ -22,7 +22,6 @@
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
@ -30,11 +29,14 @@
<hr class="primary">
</div>
</div>
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
@if (TempData.ContainsKey("TempDataProperty-StatusMessage"))
{
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
</div>
</div>
</div>
}
<div class="row">
<div class="col-lg-12">
<form method="post">
@ -141,7 +143,6 @@
</button>
</h2>
</div>
<div id="accordian-dev-info-embed-payment-button" class="collapse" aria-labelledby="accordian-dev-info-embed-payment-button-header" data-parent="#accordian-dev-info">
<div class="card-body">
<p>You can host point of sale buttons in an external website with the following code.</p>
@ -201,22 +202,18 @@
</div>
</div>
</div>
</div>
</form>
<a asp-action="ListApps">Back to the app list</a>
</div>
</div>
</div>
</section>
@section Scripts {
<link rel="stylesheet" href="~/vendor/highlightjs/default.min.css">
<script src="~/vendor/highlightjs/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
<script id="template-product-item" type="text/template">
<div class="col-sm-4 col-md-3 mb-3">
<div class="card">
@ -229,7 +226,6 @@
</div>
</div>
</script>
<script id="template-product-content" type="text/template">
<div class="mb-3">
<input class="js-product-id" type="hidden" name="id" value="{id}">
@ -264,8 +260,6 @@
</div>
</div>
</script>
<script src="~/products/js/products.js"></script>
<script src="~/products/js/products.jquery.js"></script>
}

@ -69,93 +69,69 @@
</div>
</div>
</div>
<section>
<div class="container">
<div class="row">
<div class="col-lg-8 mx-auto text-center">
<h2 class="section-heading">Special thanks</h2>
<h2 class="section-heading">Donate!</h2>
<hr class="primary">
<p>Thanks to those who gave me some time to work on this project, or helped me.</p>
</div>
</div>
<div class="row">
<div class="col-lg-4 mr-auto text-center">
<a href="https://www.dglab.com/en/">
<img src="~/img/dglab.gif" height="100" />
</a>
<p>
<a href="https://www.dglab.com/en/">DG Lab</a>
</p>
</div>
<div class="col-lg-4 mr-auto text-center"></div>
<div class="col-lg-4 ml-auto text-center">
<a href="https://metaco.com/">
<img src="~/img/metaco.jpg" height="100" />
</a>
<p><a href="https://metaco.com/">Metaco</a></p>
</div>
<div class="col-lg-4 mr-auto text-center">
<a href="https://ibukingdom.themedia.jp/">
<img src="~/img/ibuki.png" height="100" />
</a>
<p>
<a href="https://ibukingdom.themedia.jp/">Designer</a>
</p>
<form method="POST" action="https://mainnet.demo.btcpayserver.org/api/v1/invoices">
<input type="hidden" name="storeId" value="EErYwCthBNfJUpuU1etH1uhg3x1YVH1q1F2zez7u1AAX" />
<div style="text-align:center;display:inline;width:209px"><div><button style="cursor:pointer; font-size:25px; line-height: 25px; background: rgba(0,0,0,.1); height: 30px; width: 45px; border:none; border-radius: 60px; margin: auto;" onclick="event.preventDefault(); var price = parseInt(document.querySelector('#btcpay-input-price').value); if ('-' == '-' && (price - 1) < 1) { return; } document.querySelector('#btcpay-input-price').value = parseInt(document.querySelector('#btcpay-input-price').value) - 1;">-</button><input type="text" id="btcpay-input-price" name="price" value="10" style="border: none; background-image: none; background-color: transparent; -webkit-box-shadow: none ; -moz-box-shadow: none ; -webkit-appearance: none ; width: 3em; text-align: center; font-size: 25px; margin: auto; border-radius: 5px; line-height: 35px; background: #fff;" oninput="event.preventDefault();isNaN(event.target.value) || event.target.value <= 0 ? document.querySelector('#btcpay-input-price').value = 10 : event.target.value" /><button style="cursor:pointer; font-size:25px; line-height: 25px; background: rgba(0,0,0,.1); height: 30px; width: 45px; border:none; border-radius: 60px; margin: auto;" onclick="event.preventDefault(); var price = parseInt(document.querySelector('#btcpay-input-price').value); if ('+' == '-' && (price - 1) < 1) { return; } document.querySelector('#btcpay-input-price').value = parseInt(document.querySelector('#btcpay-input-price').value) + 1;">+</button></div><select onchange="document.querySelector('input[type = hidden][name = currency]').value = event.target.value" style="-webkit-appearance: none; border: 0; display: block; padding: 0 3em; margin: auto auto 5px auto; font-size: 11px; background: 0 0; cursor: pointer;"><option value="USD">USD</option><option value="GBP">GBP</option><option value="EUR">EUR</option><option value="BTC">BTC</option></select></div>
<input type="hidden" name="currency" value="USD" />
<input type="image" src="~/img/paybutton/pay.png" name="submit" style="width:209px" alt="Pay with BtcPay, Self-Hosted Bitcoin Payment Processor">
</form>
</div>
<div class="col-lg-4 mr-auto text-center"></div>
</div>
</div>
</section>
<div class="call-to-action bg-dark text-white">
<div class="container text-center">
<h2>Donate</h2>
<p>Donation to this address will be reinvested into the development of this tool</p>
<p><img src="~/img/donation.jpg"></p>
<p>3BpfdkF93GwFRWdrAN3SNsRAsi6d158YQi</p>
<div class="container">
<div class="row">
<div class="col-lg-8 mx-auto text-center">
<h2 class="section-heading">Let's Get In Touch!</h2>
<hr class="primary">
<p>
An open source project is nothing without its community<br />
Come and get in touch with us!
</p>
</div>
</div>
<div class="row">
<div class="col-lg-3 ml-auto text-center">
<a href="https://chat.btcpayserver.org/" target="_blank">
<img src="~/img/mattermost.png" height="100" />
</a>
<p><a href="https://chat.btcpayserver.org/" target="_blank">On Mattermost</a></p>
</div>
<div class="col-lg-3 ml-auto text-center">
<a href="https://slack.btcpayserver.org/" target="_blank">
<img src="~/img/slack.png" height="100" />
</a>
<p><a href="http://slack.forkbitpay.ninja/" target="_blank">On Slack</a></p>
</div>
<div class="col-lg-3 mr-auto text-center">
<a href="https://twitter.com/BtcpayServer" target="_blank">
<img src="~/img/twitter.png" height="100" />
</a>
<p>
<a href="https://twitter.com/BtcpayServer" target="_blank">On Twitter</a>
</p>
</div>
<div class="col-lg-3 mr-auto text-center">
<a href="https://github.com/btcpayserver/btcpayserver" target="_blank">
<img src="~/img/github.png" height="100" />
</a>
<p>
<a href="https://github.com/btcpayserver/btcpayserver" target="_blank">On Github</a>
</p>
</div>
</div>
</div>
<section id="contact">
<div class="container">
<div class="row">
<div class="col-lg-8 mx-auto text-center">
<h2 class="section-heading">Let's Get In Touch!</h2>
<hr class="primary">
<p>
An open source project is nothing without its community<br />
Come and get in touch with us!
</p>
</div>
</div>
<div class="row">
<div class="col-lg-3 ml-auto text-center">
<a href="https://chat.btcpayserver.org/" target="_blank">
<img src="~/img/mattermost.png" height="100" />
</a>
<p><a href="https://chat.btcpayserver.org/" target="_blank">On Mattermost</a></p>
</div>
<div class="col-lg-3 ml-auto text-center">
<a href="https://slack.btcpayserver.org/" target="_blank">
<img src="~/img/slack.png" height="100" />
</a>
<p><a href="http://slack.forkbitpay.ninja/" target="_blank">On Slack</a></p>
</div>
<div class="col-lg-3 mr-auto text-center">
<a href="https://twitter.com/BtcpayServer" target="_blank">
<img src="~/img/twitter.png" height="100" />
</a>
<p>
<a href="https://twitter.com/BtcpayServer" target="_blank">On Twitter</a>
</p>
</div>
<div class="col-lg-3 mr-auto text-center">
<a href="https://github.com/btcpayserver/btcpayserver" target="_blank">
<img src="~/img/github.png" height="100" />
</a>
<p>
<a href="https://github.com/btcpayserver/btcpayserver" target="_blank">On Github</a>
</p>
</div>
</div>
</div>
</section>

@ -182,7 +182,7 @@
computed: {
coinswitchAmountDue: function() {
return this.srvModel.coinSwitchAmountMarkupPercentage
? (1 + (this.srvModel.coinSwitchAmountMarkupPercentage / 100))
? this.srvModel.btcDue * (1 + (this.srvModel.coinSwitchAmountMarkupPercentage / 100))
: this.srvModel.btcDue;
}
}

@ -5,8 +5,8 @@
@section HeaderContent{
<META NAME="robots" CONTENT="noindex,nofollow">
}
<style type="text/css">
.linethrough {
text-decoration: line-through;
}
@ -15,15 +15,16 @@
width: 140px;
}
</style>
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="StatusMessage" />
@if (!string.IsNullOrEmpty(Model.StatusMessage))
{
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="StatusMessage" />
</div>
</div>
</div>
}
<div class="row">
<div class="col-lg-12 text-center">
@ -90,7 +91,6 @@
</tr>
</table>
</div>
<div class="col-md-6">
<h3>Buyer information</h3>
<table class="table table-sm table-responsive-md removetopborder">

@ -2,20 +2,20 @@
@{
ViewData["Title"] = "Invoices";
}
@section HeadScripts {
<script src="~/modal/btcpay.js"></script>
}
@Html.HiddenFor(a=>a.Count)
@Html.HiddenFor(a => a.Count)
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="StatusMessage" />
@if (!string.IsNullOrEmpty(Model.StatusMessage))
{
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="StatusMessage" />
</div>
</div>
</div>
}
<div class="row">
<div class="col-lg-12 text-center">
@ -48,7 +48,6 @@
<div class="row no-gutter" style="margin-bottom: 5px;">
<div class="col-lg-6">
<a asp-action="CreateInvoice" class="btn btn-primary" role="button" id="CreateNewInvoice"><span class="fa fa-plus"></span> Create a new invoice</a>
<a class="btn btn-primary dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Export
</a>
@ -60,7 +59,6 @@
<a asp-action="Export" asp-route-timezoneoffset="0" asp-route-format="json" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item export-link" target="_blank">JSON</a>
</div>
</div>
<div class="col-lg-6">
<div class="form-group">
<form asp-action="ListInvoices" method="get" style="float:right;">
@ -274,7 +272,6 @@
}
</tbody>
</table>
<nav aria-label="..." class="w-100">
<ul class="pagination float-left">
<li class="page-item @(Model.Skip == 0 ? "disabled" : null)">
@ -287,7 +284,6 @@
<a class="page-link" href="@listInvoices(1, Model.Count)">&raquo;</a>
</li>
</ul>
<ul class="pagination float-right">
<li class="page-item disabled">
<span class="page-link">Page Size:</span>
@ -328,7 +324,6 @@
</div>
</div>
<script type="text/javascript">
$(function () {
var timezoneOffset = new Date().getTimezoneOffset();
@ -378,8 +373,8 @@
});
}
</script>
<style type="text/css">
.invoice-payments h3 {
font-size: 15px;
font-weight: bold;

@ -3,7 +3,6 @@
ViewData.SetActivePageAndTitle(ManageNavPages.ChangePassword, "Change password");
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">
<div class="col-md-6">

@ -3,7 +3,6 @@
ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Enable authenticator");
}
<h4>@ViewData["Title"]</h4>
<div>
<p>To use an authenticator app go through the following steps:</p>
<ol class="list">

@ -3,7 +3,6 @@
ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Recovery codes");
}
<h4>@ViewData["Title"]</h4>
<div class="alert alert-warning" role="alert">
<p>
<span class="fa fa-warning"></span>

@ -3,7 +3,6 @@
ViewData.SetActivePageAndTitle(ManageNavPages.Index, "Profile");
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">

@ -2,7 +2,6 @@
ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Reset authenticator key");
}
<h4>@ViewData["Title"]</h4>
<div class="alert alert-warning" role="alert">
<p>
<span class="fa fa-warning"></span>

@ -3,7 +3,6 @@
ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Two-factor authentication");
}
<h4>@ViewData["Title"]</h4>
@if(Model.Is2faEnabled)
{
if(Model.RecoveryCodesLeft == 0)

@ -2,7 +2,6 @@
@using System.Globalization
@model BTCPayServer.Models.PaymentRequestViewModels.UpdatePaymentRequestViewModel
@addTagHelper *, BundlerMinifier.TagHelpers
<section>
<div class="container">
<div class="row">
@ -11,40 +10,40 @@
<hr class="primary">
</div>
</div>
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="StatusMessage"/>
@if (!string.IsNullOrEmpty(Model.StatusMessage))
{
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="StatusMessage" />
</div>
</div>
</div>
}
<div class="row">
<div class="col-lg-12">
<form method="post" action="@Url.Action("EditPaymentRequest", "PaymentRequest", new { id = Model.Id}, Context.Request.Scheme)">
<input type="hidden" name="Id" value="@Model.Id"/>
<input type="hidden" name="Id" value="@Model.Id" />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Title" class="control-label"></label>*
<input asp-for="Title" class="form-control"/>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Amount" class="control-label"></label>*
<input type="number" step="any" asp-for="Amount" class="form-control"/>
<input type="number" step="any" asp-for="Amount" class="form-control" />
<span asp-validation-for="Amount" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Currency" class="control-label"></label>*
<input asp-for="Currency" class="form-control"/>
<input asp-for="Currency" class="form-control" />
<span asp-validation-for="Currency" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="AllowCustomPaymentAmounts"></label>
<input asp-for="AllowCustomPaymentAmounts" type="checkbox" class="form-check"/>
<input asp-for="AllowCustomPaymentAmounts" type="checkbox" class="form-check" />
<span asp-validation-for="AllowCustomPaymentAmounts" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="StoreId" class="control-label"></label>
@if (string.IsNullOrEmpty(Model.Id))
{
@ -52,12 +51,10 @@
}
else
{
<input type="hidden" asp-for="StoreId" value="@Model.StoreId"/>
<input type="text" class="form-control" value="@Model.Stores.Single(item => item.Value == Model.StoreId).Text" readonly/>
<input type="hidden" asp-for="StoreId" value="@Model.StoreId" />
<input type="text" class="form-control" value="@Model.Stores.Single(item => item.Value == Model.StoreId).Text" readonly />
}
<span asp-validation-for="StoreId" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Email" class="control-label"></label>
@ -67,12 +64,10 @@
<div class="form-group">
<label asp-for="ExpiryDate" class="control-label"></label>
<div class="input-group ">
<input asp-for="ExpiryDate"
<input asp-for="ExpiryDate"
value="@( Model.ExpiryDate?.ToString("u", CultureInfo.InvariantCulture))"
class="form-control flatdtpicker" min="today" placeholder="No expiry date has been set for this payment request"/>
class="form-control flatdtpicker" min="today" placeholder="No expiry date has been set for this payment request" />
<div class="input-group-append">
<button class="btn btn-secondary input-group-clear" type="button" title="Clear">
<span class=" fa fa-times"></span>
</button>
@ -90,7 +85,7 @@
<a href="https://docs.btcpayserver.org/development/theme#bootstrap-themes" target="_blank">
<span class="fa fa-question-circle-o" title="More information..."></span>
</a>
<input asp-for="CustomCSSLink" class="form-control"/>
<input asp-for="CustomCSSLink" class="form-control" />
<span asp-validation-for="CustomCSSLink" class="text-danger"></span>
</div>
<div class="form-group">
@ -98,18 +93,17 @@
<textarea asp-for="EmbeddedCSS" rows="10" cols="40" class="form-control"></textarea>
<span asp-validation-for="EmbeddedCSS" class="text-danger"></span>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary" id="SaveButton">Save</button>
@if (!string.IsNullOrEmpty(Model.Id))
{
<a class="btn btn-secondary" target="_blank" asp-action="ViewPaymentRequest" id="@Model.Id" name="ViewAppButton">View</a>
<a class="btn btn-secondary"
<a class="btn btn-secondary"
target="_blank"
asp-action="ListInvoices"
asp-controller="Invoice"
asp-controller="Invoice"
asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(Model.Id)}")">Invoices</a>
<a class="btn btn-secondary" asp-action="ClonePaymentRequest" id="@Model.Id">Clone</a>
<a class="btn btn-secondary" asp-action="ClonePaymentRequest" id="@Model.Id">Clone</a>
}
<a class="btn btn-secondary" target="_blank" asp-action="GetPaymentRequests">Back to list</a>
</div>
@ -118,9 +112,8 @@
</div>
</div>
</section>
@section Scripts {
<script src= "~/vendor/moment/moment.js"></script>
<script src="~/vendor/moment/moment.js"></script>
<bundle name="wwwroot/bundles/payment-request-admin-bundle.min.js"></bundle>
<bundle name="wwwroot/bundles/payment-request-admin-bundle.min.css"></bundle>
}

@ -1,69 +1,66 @@
@using BTCPayServer.Services.PaymentRequests
@model BTCPayServer.Models.PaymentRequestViewModels.ListPaymentRequestsViewModel
@{
Layout = "_Layout";
}
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="StatusMessage"/>
@if (!string.IsNullOrEmpty(Model.StatusMessage))
{
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="StatusMessage" />
</div>
</div>
</div>
}
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">Payment Requests</h2>
</div>
</div>
<div class="row no-gutter" style="margin-bottom: 5px;">
<div class="col-lg-6">
<a asp-action="EditPaymentRequest" class="btn btn-primary" role="button" id="CreatePaymentRequest"><span class="fa fa-plus"></span> Create a new payment request</a>
<a href="https://docs.btcpayserver.org/features/paymentrequests" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
</div>
</div>
<div class="row">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Title</th>
<th>Expiry</th>
<th class="text-right">Price</th>
<th class="text-right">Status</th>
<th class="text-right">Actions</th>
</tr>
<tr>
<th>Title</th>
<th>Expiry</th>
<th class="text-right">Price</th>
<th class="text-right">Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr>
<td>@item.Title</td>
<td>@(item.ExpiryDate?.ToString("g") ?? "No Expiry")</td>
<td class="text-right">@item.Amount @item.Currency</td>
<td class="text-right">@item.Status</td>
<td class="text-right">
<a asp-action="EditPaymentRequest" asp-route-id="@item.Id">Edit</a>
<span> - </span>
<a asp-action="ViewPaymentRequest" asp-route-id="@item.Id">View</a>
<span> - </span>
<a target="_blank" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(item.Id)}")">Invoices</a>
<span> - </span>
<a target="_blank" asp-action="PayPaymentRequest" asp-route-id="@item.Id">Pay</a>
<span> - </span>
<a target="_blank" asp-action="ClonePaymentRequest" asp-route-id="@item.Id">Clone</a>
<span> - </span>
<a asp-action="RemovePaymentRequestPrompt" asp-route-id="@item.Id">Remove</a>
</td>
</tr>
}
@foreach (var item in Model.Items)
{
<tr>
<td>@item.Title</td>
<td>@(item.ExpiryDate?.ToString("g") ?? "No Expiry")</td>
<td class="text-right">@item.Amount @item.Currency</td>
<td class="text-right">@item.Status</td>
<td class="text-right">
<a asp-action="EditPaymentRequest" asp-route-id="@item.Id">Edit</a>
<span> - </span>
<a asp-action="ViewPaymentRequest" asp-route-id="@item.Id">View</a>
<span> - </span>
<a target="_blank" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(item.Id)}")">Invoices</a>
<span> - </span>
<a target="_blank" asp-action="PayPaymentRequest" asp-route-id="@item.Id">Pay</a>
<span> - </span>
<a target="_blank" asp-action="ClonePaymentRequest" asp-route-id="@item.Id">Clone</a>
<span> - </span>
<a asp-action="RemovePaymentRequestPrompt" asp-route-id="@item.Id">Remove</a>
</td>
</tr>
}
</tbody>
</table>
<nav aria-label="...">
<ul class="pagination">
<li class="page-item @(Model.Skip == 0 ? "disabled" : null)">
@ -86,6 +83,5 @@
</ul>
</nav>
</div>
</div>
</section>

@ -6,7 +6,6 @@
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]"/>
<div class="row">
<div class="col-lg-6">

@ -4,7 +4,6 @@
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
<div class="row">
<div class="col-lg-6">

@ -4,7 +4,6 @@
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
<div class="row">
<div class="col-lg-6">

@ -3,21 +3,16 @@
ViewData.SetActivePageAndTitle(ServerNavPages.Services, $"Storage - Local Filesystem");
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
<partial name="_StatusMessage" for="@TempData["StatusMessage"]"/>
<div class="row">
<div class="col-lg-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<p>Nothing to configure here. Data will be saved in the btcpay data directory.</p>
<p>Any uploaded files are being saved on the same machine that hosts BTCPay; please pay attention to your storage space.</p>
<div class="row">
<div class="col-lg-6">
<form method="post">
<button type="submit" class="btn btn-primary" name="command" value="Save">Choose</button>
<a asp-action="Storage" asp-route-forceChoice="true" >Change Storage provider</a>
</form>
<a asp-action="Storage" asp-route-forceChoice="true">Change Storage provider</a>
</div>
</div>

@ -3,7 +3,6 @@
ViewData.SetActivePageAndTitle(ServerNavPages.Services, $"Storage - Google Cloud Storage");
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
<div class="row">
<div class="col-lg-6">

@ -4,39 +4,42 @@
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]"/>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
<h4>File Storage</h4>
<div class="form-group">
<span>Change your <a asp-action="Services" asp-route-returnurl="@ViewData["ReturnUrl"]">external storage service</a> provider</span>
<a href="https://docs.btcpayserver.org/faq-and-common-issues/faq-serversettings#how-to-upload-files-to-btcpay" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
</div>
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Name</th>
<th>Timestamp</th>
<th>User</th>
<th></th>
</tr>
<tr>
<th>Name</th>
<th>Timestamp</th>
<th>User</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var file in Model.Files)
{
<tr>
<td>@file.FileName</td>
<td >@file.Timestamp.ToBrowserDate2()</td>
<td>@file.ApplicationUser.UserName</td>
<td>
<a asp-action="Files" asp-route-fileId="@file.Id">Get Link</a>
- <a asp-action="CreateTemporaryFileUrl" asp-route-fileId="@file.Id">Get Temp Link</a>
- <a asp-action="DeleteFile" asp-route-fileId="@file.Id">Remove</a>
</td>
</tr>
}
@if (!Model.Files.Any())
{
<tr>
<td colspan="4" class="text-center">No files</td>
</tr>
}
@foreach (var file in Model.Files)
{
<tr>
<td>@file.FileName</td>
<td>@file.Timestamp.ToBrowserDate2()</td>
<td>@file.ApplicationUser.UserName</td>
<td>
<a asp-action="Files" asp-route-fileId="@file.Id">Get Link</a>
- <a asp-action="CreateTemporaryFileUrl" asp-route-fileId="@file.Id">Get Temp Link</a>
- <a asp-action="DeleteFile" asp-route-fileId="@file.Id">Remove</a>
</td>
</tr>
}
@if (!Model.Files.Any())
{
<tr>
<td colspan="4" class="text-center">No files</td>
</tr>
}
</tbody>
</table>
@ -92,21 +95,21 @@
</form>
</div>
@section Scripts {
<script>
$(document).ready(function() {
@section Scripts {
<script>
$(document).ready(function () {
$('.custom-file-input').on('change',
function() {
var label = $(this).next('label');
if (document.getElementById("file").files.length > 0) {
var fileName = document.getElementById("file").files[0].name;
label.addClass("selected").html(fileName);
} else {
label.removeClass("selected").html("Choose file");
}
});
});
</script>
}
$('.custom-file-input').on('change',
function () {
var label = $(this).next('label');
if (document.getElementById("file").files.length > 0) {
var fileName = document.getElementById("file").files[0].name;
label.addClass("selected").html(fileName);
} else {
label.removeClass("selected").html("Choose file");
}
});
});
</script>
}
}

@ -7,7 +7,7 @@
<div class="row">
<div class="col">
<h4>@ViewData["Title"]</h4>
</div>
<div class="col text-right">
<a

@ -3,7 +3,6 @@
ViewData.SetActivePageAndTitle(ServerNavPages.Logs);
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="StatusMessage"/>
<div class="row">

@ -4,7 +4,6 @@
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
<div class="row">

@ -4,37 +4,95 @@
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
<div class="row">
<div class="col-lg-6">
<div asp-validation-summary="All" class="text-danger"></div>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]"/>
@if (!this.ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
}
<form method="post">
<div class="form-group">
<label asp-for="RequiresConfirmedEmail"></label>
<input asp-for="RequiresConfirmedEmail" type="checkbox" class="form-check-inline"/>
<span asp-validation-for="RequiresConfirmedEmail" class="text-danger"></span>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<form method="post">
<div class="form-group">
<label asp-for="RequiresConfirmedEmail"></label>
<input asp-for="RequiresConfirmedEmail" type="checkbox" class="form-check-inline" />
</div>
<div class="form-group">
<label asp-for="LockSubscription"></label>
<input asp-for="LockSubscription" type="checkbox" class="form-check-inline" />
</div>
<div class="form-group">
<label asp-for="DiscourageSearchEngines"></label>
<input asp-for="DiscourageSearchEngines" type="checkbox" class="form-check-inline" />
</div>
<div class="form-group">
<label asp-for="RootAppId"></label>
<select asp-for="RootAppId" asp-items="ViewBag.AppsList" class="form-control"></select>
</div>
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
</form>
<div class="form-group">
<label asp-for="LockSubscription"></label>
<input asp-for="LockSubscription" type="checkbox" class="form-check-inline"/>
<span asp-validation-for="LockSubscription" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="DiscourageSearchEngines"></label>
<input asp-for="DiscourageSearchEngines" type="checkbox" class="form-check-inline"/>
<span asp-validation-for="DiscourageSearchEngines" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="RootAppId"></label>
<select asp-for="RootAppId" asp-items="@(new SelectList(ViewBag.AppsList, nameof(SelectListItem.Value), nameof(SelectListItem.Text), Model.RootAppId))" class="form-control"></select>
@if (!Model.DomainToAppMapping.Any())
{
<button type="submit" name="command" value="add-domain" class="btn btn-link"> Map specific domains to specific apps</button>
}
</div>
@if (Model.DomainToAppMapping.Any())
{
<div class="list-group mb-2">
<div class="list-group-item">
<h5 class="mb-1">
Domain to app mapping
<button type="submit" name="command" value="add-domain" class="ml-1 btn btn-secondary btn-sm ">Add domain mapping </button>
</h5>
</div>
@for (var index = 0; index < Model.DomainToAppMapping.Count; index++)
{
<div class="list-group-item p-0 pl-lg-2">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-10 py-2 ">
<div class="form-group">
<label asp-for="DomainToAppMapping[index].Domain" class="control-label"></label>
<input asp-for="DomainToAppMapping[index].Domain" class="form-control"/>
<span asp-validation-for="DomainToAppMapping[index].Domain" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DomainToAppMapping[index].AppId"></label>
<select asp-for="DomainToAppMapping[index].AppId"
asp-items="@(new SelectList(ViewBag.AppsList,
nameof(SelectListItem.Value),
nameof(SelectListItem.Text),
Model.DomainToAppMapping[index].AppId))"
class="form-control">
</select>
<span asp-validation-for="DomainToAppMapping[index].AppId" class="text-danger"></span>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-2 pull-right">
<button type="submit" title="Remove domain mapping" name="command" value="@($"remove-domain:{index}")"
class="d-block d-lg-none d-xl-none btn btn-danger mb-2 ml-2">
Remove Destination
</button>
<button type="submit" title="Remove domain mapping" name="command" value="@($"remove-domain:{index}")"
class="d-none d-lg-block remove-domain-btn text-decoration-none h-100 align-middle btn text-danger btn-link fa fa-times rounded-0 pull-right">
</button>
</div>
</div>
</div>
}
</div>
}
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
</form>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
<style>
.remove-domain-btn{
font-size: 1.5rem;
border-radius: 0;
}
.remove-domain-btn:hover{
background-color: #CCCCCC;
}
</style>
}

Some files were not shown because too many files have changed in this diff Show More