From eb16b435df59e98f25bafeb2b7311f2b69cd6f98 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Fri, 22 Nov 2019 22:58:51 +0100 Subject: [PATCH 001/810] Document themeing approach Sums up and adds to the comments made in #1175. I wasn't sure where to put these kinds of docs, as tehy are code related and not end user facing. Let's discuss whether or not this fits in here or should become part of the docs repo. fix --- BTCPayServer/wwwroot/main/themes/casa.css | 2 +- BTCPayServer/wwwroot/main/themes/classic.css | 8 +-- Docs/Themes.md | 51 ++++++++++++++++++++ README.md | 10 ++-- 4 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 Docs/Themes.md diff --git a/BTCPayServer/wwwroot/main/themes/casa.css b/BTCPayServer/wwwroot/main/themes/casa.css index 07c8c3f80..e19bc8e47 100644 --- a/BTCPayServer/wwwroot/main/themes/casa.css +++ b/BTCPayServer/wwwroot/main/themes/casa.css @@ -6,6 +6,7 @@ --btcpay-brand-darker: #19154B; --btcpay-brand-darkest: #02000C; + /* Color definitions for specific purposes - map the general colors or define additional ones */ --btcpay-color-white: #fff; --btcpay-color-black: #000; @@ -19,7 +20,6 @@ --btcpay-color-neutral-800: #181334; --btcpay-color-neutral-900: #100d20; - /* Color definitions for specific purposes - map the general colors or define additional ones */ --btcpay-color-primary: var(--btcpay-brand-medium); /* Usage: Text color */ --btcpay-color-primary-backdrop: #baa4fd; /* Usage: Backgrounds, borders, shadows */ --btcpay-color-primary-accent: #4b3bc0; /* Usage: Background on Focus/Hover */ diff --git a/BTCPayServer/wwwroot/main/themes/classic.css b/BTCPayServer/wwwroot/main/themes/classic.css index 5e8b2cd4f..cd04c764a 100644 --- a/BTCPayServer/wwwroot/main/themes/classic.css +++ b/BTCPayServer/wwwroot/main/themes/classic.css @@ -6,6 +6,7 @@ --btcpay-brand-darker: #0F3B21; --btcpay-brand-darkest: #05120a; + /* Color definitions for specific purposes - map the general colors or define additional ones */ --btcpay-color-white: #fff; --btcpay-color-black: #000; @@ -19,10 +20,9 @@ --btcpay-color-neutral-800: #343a40; --btcpay-color-neutral-900: #212529; - /* Color definitions for specific purposes - map the general colors or define additional ones */ - --btcpay-color-primary: #329f80; - --btcpay-color-primary-backdrop: rgba(81, 173, 147, 0.25); - --btcpay-color-primary-accent: #267861; + --btcpay-color-primary: #329f80; /* Usage: Text color */ + --btcpay-color-primary-backdrop: rgba(81, 173, 147, 0.25); /* Usage: Backgrounds, borders, shadows */ + --btcpay-color-primary-accent: #267861; /* Usage: Background on Focus/Hover */ --btcpay-color-secondary: var(--btcpay-color-neutral-600); --btcpay-color-secondary-backdrop: rgba(130, 138, 145, 0.25); --btcpay-color-secondary-accent: var(--btcpay-color-neutral-800); diff --git a/Docs/Themes.md b/Docs/Themes.md new file mode 100644 index 000000000..7f61e8a77 --- /dev/null +++ b/Docs/Themes.md @@ -0,0 +1,51 @@ +# Developing and extending themes + +The BTCPay Server user interface is built on a customized version of Bootstrap that supports [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/--*). +This allows us to change theme related settings like fonts and colors without affecting the [`bootstrap.css`](#Notes-on-bootstrapcss). +Also we can provide just the relevant customized parts instead of shipping a whole `bootstrap.css` file for each theme. + +Take a look at the [predefined themes](../BTCPayServer/wwwroot/main/themes) to get an overview of this approach. + +## Modifying existing themes + +The custom property definitions in the `:root` selector are divided into several sections, that can be seen as a cascade: + +- The first section contains general definitions (i.e. for custom brand and neutral colors). +- The second section defines variables for specific purposes. + Here you can map the general definitions or create additional ones. +- The third section contains definitions for specific parts of the page, sections or components. + Here you should try to reuse definitions from above as much as possible to provide a consistent look and feel. + +The variables defined in a theme file get used in the [`site.css`](../BTCPayServer/wwwroot/main/site.css) and [`creative.css`](../BTCPayServer/wwwroot/main/bootstrap4-creativestart/creative.css) files. + +### Overriding Bootstrap selectors + +In addition to the variables you can also provide styles by directly adding CSS selectors to this file. +This can be seen as a last resort in case there is no variable for something you want to change or some minor tweaking. + +### Adding theme variables + +In general it is a good idea to introduce specific variables for special purposes (like setting the link colors of a specific section). +This allows us to address individual portions of the styles without affecting other parts which might be tight to a general variable. + +For cases in which you want to introduce new variables that are used across all themes, add them to the `site.css` file. +This file contains our modifications of the Bootstrap styles. +Refrain from modifying `bootstrap.css` directly – see the [additional notes](#Notes-on-bootstrapcss) for the reasoning behind this. + +## Adding a new theme + +You should copy one of our predefined themes and change the variables to fit your needs. + +To test and play around with the adjustments, you can also use the developer tools of the browser: +Inspect the `` element and modify the variables in the `:root` section of the styles inspector. + +## Notes on bootstrap.css + +The `bootstrap.css` file itself is generated based on what the original vendor `bootstrap.css` provides. + +Right now [Bootstrap](https://getbootstrap.com/docs/4.3/getting-started/theming/) does not use custom properties, but in the future it is likely that they might switch to this approach as well. +Until then we created a build script [in this repo](https://github.com/dennisreimann/btcpayserver-ui-prototype) which generates the `bootstrap.css` file we are using here. + +The general approach should be to not modify the `bootstrap.css`, so that we can keep it easily updatable. +The initial modifications of this file were made in order to allow for this themeing approach. +Because bootstrap has colors spread all over the place we'd otherwise have to override mostly everything, that's why these general modifications are in the main `bootstrap.css` file. diff --git a/README.md b/README.md index 768fd26de..eecc4df73 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ # BTCPay Server -## Introduction +## Introduction BTCPay Server is a free and open-source cryptocurrency payment processor which allows you to receive payments in Bitcoin and altcoins directly, with no fees, transaction cost or a middleman. @@ -58,13 +58,13 @@ Thanks to the [apps](https://github.com/btcpayserver/btcpayserver-doc/blob/maste ## Getting Started -Firstly, decide if you want to host an instance yourself or use a [third-party host](https://docs.btcpayserver.org/deployment/thirdpartyhosting). If you've chosen to self-host, we documented plenty [ways to deploy BTCPay Server](https://docs.btcpayserver.org/deployment/deployment). +Firstly, decide if you want to host an instance yourself or use a [third-party host](https://docs.btcpayserver.org/deployment/thirdpartyhosting). If you've chosen to self-host, we documented plenty [ways to deploy BTCPay Server](https://docs.btcpayserver.org/deployment/deployment). After successful deployment, make sure to check our [getting started](https://docs.btcpayserver.org/getting-started/registeraccount) and [walkthrough](https://docs.btcpayserver.org/btcpay-basics/walkthrough) guides. In case you would like to use Lightning Network, see [Lightning guide](https://docs.btcpayserver.org/features/lightningnetwork). ## Documentation -Please check out our [official website](https://btcpayserver.org/), our [complete documentation](https://github.com/btcpayserver/btcpayserver-doc) and [FAQ](https://github.com/btcpayserver/btcpayserver-doc/tree/master/FAQ#btcpay-frequently-asked-questions-and-common-issues) for more details. +Please check out our [official website](https://btcpayserver.org/), our [complete documentation](https://github.com/btcpayserver/btcpayserver-doc) and [FAQ](https://github.com/btcpayserver/btcpayserver-doc/tree/master/FAQ#btcpay-frequently-asked-questions-and-common-issues) for more details. If you have trouble using BTCPay, consider joining [communities listed on official website](https://btcpayserver.org/#communityCTA) to get help from BTCPay community members. Only file [Github issue](https://github.com/btcpayserver/btcpayserver/issues) for technical issues you can't resolve through other channels or feature requests you've validated with other members of community. @@ -82,6 +82,8 @@ Contributors looking to do something a bit more challenging, before opening a pu You also have an awesome video of our contributors which explains how to get started.[![Rockstar Dev and Britt Kelly - Btc Pay Server Code Along](https://img.youtube.com/vi/ZePbMPSIvHM/sddefault.jpg)](https://www.youtube.com/embed/VNMnd-dX9Q8) +Here is some info about [how to extend the themes](Docs/Themes.md) + ## How to build While the documentation advises to use docker-compose, you may want to build BTCPay yourself. @@ -183,4 +185,4 @@ The BTCPay Server Project is proudly supported by these entities through the [BT -If you'd like to support the project, please visit the [donation page](https://btcpayserver.org/donate/). \ No newline at end of file +If you'd like to support the project, please visit the [donation page](https://btcpayserver.org/donate/). From 564dd95d81ea11c51e20eccdebfcf18b6d3df710 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Fri, 22 Nov 2019 23:00:03 +0100 Subject: [PATCH 002/810] Remove unused modern theme We can start over with this once the general design decisions are decided on and we start implementing a new UI. --- BTCPayServer/wwwroot/main/themes/modern.css | 76 --------------------- 1 file changed, 76 deletions(-) delete mode 100644 BTCPayServer/wwwroot/main/themes/modern.css diff --git a/BTCPayServer/wwwroot/main/themes/modern.css b/BTCPayServer/wwwroot/main/themes/modern.css deleted file mode 100644 index b1cf22876..000000000 --- a/BTCPayServer/wwwroot/main/themes/modern.css +++ /dev/null @@ -1,76 +0,0 @@ -:root { - --btcpay-color-white: #fff; - --btcpay-color-black: #000; - - --btcpay-color-neutral-100: #FBFAF8; - --btcpay-color-neutral-200: #F5F5F3; - --btcpay-color-neutral-300: #E5E5E5; - --btcpay-color-neutral-400: #ced4da; - --btcpay-color-neutral-500: #adb5bd; - --btcpay-color-neutral-600: #536E8D; - --btcpay-color-neutral-700: #465C76; - --btcpay-color-neutral-800: #244B71; - --btcpay-color-neutral-900: #202C39; - - --btcpay-color-primary: #329f80; - --btcpay-color-primary-accent: #267861; - --btcpay-color-primary-backdrop: rgba(81, 173, 147, 0.2); - --btcpay-color-secondary: var(--btcpay-color-neutral-600); - --btcpay-color-secondary-accent: var(--btcpay-color-neutral-800); - --btcpay-color-secondary-backdrop: rgba(130, 138, 145, 0.25); - --btcpay-color-success: #329f80; - --btcpay-color-success-accent: #1e7e34; - --btcpay-color-success-backdrop: rgba(72, 180, 97, 0.25); - --btcpay-color-info: #17a2b8; - --btcpay-color-info-accent: #117a8b; - --btcpay-color-info-backdrop: rgba(58, 176, 195, 0.25); - --btcpay-color-warning: #ffc107; - --btcpay-color-warning-accent: #d39e00; - --btcpay-color-warning-backdrop: rgba(222, 170, 12, 0.25); - --btcpay-color-danger: #dc3545; - --btcpay-color-danger-accent: #bd2130; - --btcpay-color-danger-backdrop: rgba(225, 83, 97, 0.25); - --btcpay-color-light: var(--btcpay-color-neutral-100); - --btcpay-color-light-accent: #dae0e5; - --btcpay-color-light-backdrop: rgba(216, 217, 219, 0.25); - --btcpay-color-dark: var(--btcpay-color-neutral-900); - --btcpay-color-dark-accent: #1d2124; - --btcpay-color-dark-backdrop: rgba(82, 88, 93, 0.25); - - --btcpay-bg-body: var(--btcpay-color-neutral-100); - --btcpay-bg-dark: var(--btcpay-color-neutral-900); - --btcpay-bg-tile: var(--btcpay-color-white); - --btcpay-bg-cta: var(--btcpay-bg-dark); - - --btcpay-body-color: var(--btcpay-color-neutral-900); - --btcpay-body-color-link: var(--btcpay-color-primary); - --btcpay-body-color-link-accent: var(--btcpay-color-primary-accent); - - --btcpay-preformatted-text-color: var(--btcpay-color-neutral-900); - - --btcpay-font-size-base: 16px; - --btcpay-font-family-head: 'Open Sans', 'Helvetica Neue', Arial, sans-serif; - --btcpay-font-family-base: -apple-system, 'Open Sans', BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; - --btcpay-font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; -} - -/* Bootstrap modifications */ -html { - font-size: var(--btcpay-font-size-base); -} - -.bg-dark { - background-color: var(--btcpay-bg-dark) !important; -} - -header.masthead::before, -.service-box img { - filter: hue-rotate(410deg) saturate(33%); } - -.table { - margin: 1.5rem 0 3rem; -} - -.table th { - border-top: 0; -} From eb85b1a7b40d298c17975d8784933720dad6129a Mon Sep 17 00:00:00 2001 From: Matt Pazar Date: Wed, 4 Dec 2019 14:21:33 -0600 Subject: [PATCH 003/810] Fixing sats exchange rate display Fixed #1147 --- BTCPayServer/Controllers/InvoiceController.UI.cs | 4 ++++ BTCPayServer/Models/InvoicingModels/PaymentModel.cs | 1 - .../Payments/Lightning/LightningLikePaymentHandler.cs | 1 - BTCPayServer/Views/Invoice/Checkout-Body.cshtml | 3 ++- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index e5ef213e8..95bc3e728 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -294,6 +294,10 @@ namespace BTCPayServer.Controllers }; paymentMethodHandler.PreparePaymentModel(model, dto, storeBlob); + if (model.IsLightning && storeBlob.LightningAmountInSatoshi && model.CryptoCode == "Sats") + { + model.Rate = _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate / 100_000_000, paymentMethod.ParentEntity.ProductInformation.Currency); + } model.UISettings = paymentMethodHandler.GetCheckoutUISettings(); model.PaymentMethodId = paymentMethodId.ToString(); var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds); diff --git a/BTCPayServer/Models/InvoicingModels/PaymentModel.cs b/BTCPayServer/Models/InvoicingModels/PaymentModel.cs index 2e25f36e8..280a98e80 100644 --- a/BTCPayServer/Models/InvoicingModels/PaymentModel.cs +++ b/BTCPayServer/Models/InvoicingModels/PaymentModel.cs @@ -78,6 +78,5 @@ namespace BTCPayServer.Models.InvoicingModels public string RootPath { get; set; } public decimal CoinSwitchAmountMarkupPercentage { get; set; } public bool RedirectAutomatically { get; set; } - public string RateBaseAmount { get; set; } = "1"; } } diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index a6b224e74..ca085c4f6 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -185,7 +185,6 @@ namespace BTCPayServer.Payments.Lightning model.BtcPaid = Money.Parse(model.BtcPaid).ToUnit(MoneyUnit.Satoshi).ToString(CultureInfo.InvariantCulture); model.NetworkFee = new Money(model.NetworkFee, MoneyUnit.BTC).ToUnit(MoneyUnit.Satoshi); model.OrderAmount = Money.Parse(model.OrderAmount).ToUnit(MoneyUnit.Satoshi).ToString(CultureInfo.InvariantCulture); - model.RateBaseAmount = Money.FromUnit(1, MoneyUnit.BTC).Satoshi.ToString(CultureInfo.InvariantCulture); } } public override string GetCryptoImage(PaymentMethodId paymentMethodId) diff --git a/BTCPayServer/Views/Invoice/Checkout-Body.cshtml b/BTCPayServer/Views/Invoice/Checkout-Body.cshtml index b577e27d1..c17c81f70 100644 --- a/BTCPayServer/Views/Invoice/Checkout-Body.cshtml +++ b/BTCPayServer/Views/Invoice/Checkout-Body.cshtml @@ -100,7 +100,8 @@ {{ srvModel.btcDue }} {{ srvModel.cryptoCode }}
- {{srvModel.rateBaseAmount}} {{ srvModel.cryptoCodeSrv }} = {{ srvModel.rate }} + 1 Sat = {{ srvModel.rate }} + 1 {{ srvModel.cryptoCodeSrv }} = {{ srvModel.rate }}
From 2d932ebb21cdf8360188877109f890c071fd386f Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sat, 7 Dec 2019 16:35:13 +0900 Subject: [PATCH 004/810] Update to runtime .NET 2.1.14 --- BTCPayServer.Common/BTCPayServer.Common.csproj | 2 +- BTCPayServer.Data/BTCPayServer.Data.csproj | 6 +++--- BTCPayServer.Rating/BTCPayServer.Rating.csproj | 2 +- BTCPayServer/BTCPayServer.csproj | 2 +- BTCPayServer/Controllers/VaultController.cs | 2 +- amd64.Dockerfile | 4 ++-- arm32v7.Dockerfile | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/BTCPayServer.Common/BTCPayServer.Common.csproj b/BTCPayServer.Common/BTCPayServer.Common.csproj index f0a8011e3..56db03260 100644 --- a/BTCPayServer.Common/BTCPayServer.Common.csproj +++ b/BTCPayServer.Common/BTCPayServer.Common.csproj @@ -3,7 +3,7 @@ - + diff --git a/BTCPayServer.Data/BTCPayServer.Data.csproj b/BTCPayServer.Data/BTCPayServer.Data.csproj index 9589d1924..dc5159f95 100644 --- a/BTCPayServer.Data/BTCPayServer.Data.csproj +++ b/BTCPayServer.Data/BTCPayServer.Data.csproj @@ -2,9 +2,9 @@ - - - + + + diff --git a/BTCPayServer.Rating/BTCPayServer.Rating.csproj b/BTCPayServer.Rating/BTCPayServer.Rating.csproj index 1a5e55b1a..aa2a130d5 100644 --- a/BTCPayServer.Rating/BTCPayServer.Rating.csproj +++ b/BTCPayServer.Rating/BTCPayServer.Rating.csproj @@ -7,7 +7,7 @@ - + diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 58d9a13ac..8417cadcf 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -57,7 +57,7 @@ - + diff --git a/BTCPayServer/Controllers/VaultController.cs b/BTCPayServer/Controllers/VaultController.cs index 995289519..0ec1c1744 100644 --- a/BTCPayServer/Controllers/VaultController.cs +++ b/BTCPayServer/Controllers/VaultController.cs @@ -92,7 +92,7 @@ namespace BTCPayServer.Controllers try { // On trezor T this will prompt the password! (https://github.com/bitcoin-core/HWI/issues/283) - _ = device.GetXPubAsync(new KeyPath("44'"), cancellationToken); + await device.GetXPubAsync(new KeyPath("44'"), cancellationToken); } catch (HwiException ex) when (ex.ErrorCode == HwiErrorCode.DeviceAlreadyUnlocked) { diff --git a/amd64.Dockerfile b/amd64.Dockerfile index c45b6f197..84338a207 100644 --- a/amd64.Dockerfile +++ b/amd64.Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/core/sdk:2.1.505-alpine3.7 AS builder +FROM mcr.microsoft.com/dotnet/core/sdk:2.1.607-alpine3.7 AS builder WORKDIR /source COPY nuget.config nuget.config COPY Build/Common.csproj Build/Common.csproj @@ -14,7 +14,7 @@ COPY BTCPayServer/. BTCPayServer/. COPY Build/Version.csproj Build/Version.csproj RUN cd BTCPayServer && dotnet publish --output /app/ --configuration Release -FROM mcr.microsoft.com/dotnet/core/aspnet:2.1.9-alpine3.7 +FROM mcr.microsoft.com/dotnet/core/aspnet:2.1.14-alpine3.7 ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false RUN apk add --no-cache icu-libs openssh-keygen diff --git a/arm32v7.Dockerfile b/arm32v7.Dockerfile index 6fbd6cb5b..e888bb0ac 100644 --- a/arm32v7.Dockerfile +++ b/arm32v7.Dockerfile @@ -1,5 +1,5 @@ # This is a manifest image, will pull the image with the same arch as the builder machine -FROM mcr.microsoft.com/dotnet/core/sdk:2.1.505 AS builder +FROM mcr.microsoft.com/dotnet/core/sdk:2.1.607 AS builder RUN apt-get update \ && apt-get install -qq --no-install-recommends qemu qemu-user-static qemu-user binfmt-support @@ -19,7 +19,7 @@ COPY Build/Version.csproj Build/Version.csproj RUN cd BTCPayServer && dotnet publish --output /app/ --configuration Release # Force the builder machine to take make an arm runtime image. This is fine as long as the builder does not run any program -FROM mcr.microsoft.com/dotnet/core/aspnet:2.1.9-stretch-slim-arm32v7 +FROM mcr.microsoft.com/dotnet/core/aspnet:2.1.14-stretch-slim-arm32v7 COPY --from=builder /usr/bin/qemu-arm-static /usr/bin/qemu-arm-static RUN apt-get update && apt-get install -y --no-install-recommends iproute2 openssh-client \ && rm -rf /var/lib/apt/lists/* From 062c42e524ef90690a348dae9d9cb5d5a2f0fae9 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sat, 7 Dec 2019 16:42:03 +0900 Subject: [PATCH 005/810] Fix build --- BTCPayServer.Data/BTCPayServer.Data.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer.Data/BTCPayServer.Data.csproj b/BTCPayServer.Data/BTCPayServer.Data.csproj index dc5159f95..a87e1ddd2 100644 --- a/BTCPayServer.Data/BTCPayServer.Data.csproj +++ b/BTCPayServer.Data/BTCPayServer.Data.csproj @@ -4,7 +4,7 @@ - + From 452a705b7570d26bb03013e80ae1c89f2a6d3b6b Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sat, 7 Dec 2019 17:21:11 +0900 Subject: [PATCH 006/810] Update russian,portugal --- BTCPayServer/wwwroot/locales/pt-BR.json | 2 +- BTCPayServer/wwwroot/locales/ru-RU.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/BTCPayServer/wwwroot/locales/pt-BR.json b/BTCPayServer/wwwroot/locales/pt-BR.json index 27613b9a7..4c1305cb4 100644 --- a/BTCPayServer/wwwroot/locales/pt-BR.json +++ b/BTCPayServer/wwwroot/locales/pt-BR.json @@ -48,5 +48,5 @@ "Pay with Changelly": "Pagar com Changelly", "Close": "Fechar", "NotPaid_ExtraTransaction": "A fatura não foi paga integralmente. Por favor, envie outra transação para cobrir o valor devido.", - "Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte" + "Recommended_Fee": "Taxa recomendada: {{feeRate}} sat/byte" } \ No newline at end of file diff --git a/BTCPayServer/wwwroot/locales/ru-RU.json b/BTCPayServer/wwwroot/locales/ru-RU.json index 275f52ee0..898aef627 100644 --- a/BTCPayServer/wwwroot/locales/ru-RU.json +++ b/BTCPayServer/wwwroot/locales/ru-RU.json @@ -48,5 +48,5 @@ "Pay with Changelly": "Оплатить с помощью Changelly", "Close": "Закрыть", "NotPaid_ExtraTransaction": "Счет не был оплачен полностью. Пожалуйста, отправьте еще одну транзакцию для покрытия суммы задолженности.", - "Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte" + "Recommended_Fee": "Рекомендуемая комиссия: {{feeRate}} сат/байт" } \ No newline at end of file From 6addb3e481c180437b605337fdfa4abe1bb447ff Mon Sep 17 00:00:00 2001 From: Umar Bolatov Date: Sat, 7 Dec 2019 21:20:42 -0800 Subject: [PATCH 007/810] [Wallet] Show invalid address message when address is invalid fix #1217 --- BTCPayServer/Controllers/WalletsController.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index 01d7b2161..dfd7628a6 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -395,7 +395,11 @@ namespace BTCPayServer.Controllers } catch { - ModelState.AddModelError(nameof(transactionOutput.DestinationAddress), "Invalid address"); + var inputName = + string.Format(CultureInfo.InvariantCulture, "Outputs[{0}].", i.ToString(CultureInfo.InvariantCulture)) + + nameof(transactionOutput.DestinationAddress); + + ModelState.AddModelError(inputName, "Invalid address"); } if (transactionOutput.Amount.HasValue) @@ -432,7 +436,7 @@ namespace BTCPayServer.Controllers } } - if (!ModelState.IsValid) + if (!ModelState.IsValid) return View(vm); DerivationSchemeSettings derivationScheme = GetDerivationSchemeSettings(walletId); From 12055a000bd2572c56eeb8b27978fb5fef4026f1 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 10 Dec 2019 18:16:52 +0900 Subject: [PATCH 008/810] Proper handling of passphrase for Trezor T --- BTCPayServer/Controllers/VaultController.cs | 57 ++++++++------------- BTCPayServer/wwwroot/js/vaultbridge.ui.js | 29 ++--------- 2 files changed, 24 insertions(+), 62 deletions(-) diff --git a/BTCPayServer/Controllers/VaultController.cs b/BTCPayServer/Controllers/VaultController.cs index 0ec1c1744..e85b2c697 100644 --- a/BTCPayServer/Controllers/VaultController.cs +++ b/BTCPayServer/Controllers/VaultController.cs @@ -64,7 +64,6 @@ namespace BTCPayServer.Controllers HwiEnumerateEntry deviceEntry = null; HDFingerprint? fingerprint = null; string password = null; - bool pinProvided = false; var websocketHelper = new WebSocketHelper(websocket); async Task RequireDeviceUnlocking() @@ -79,39 +78,19 @@ namespace BTCPayServer.Controllers await websocketHelper.Send("{ \"error\": \"need-initialized\"}", cancellationToken); return true; } - if ((deviceEntry.Code is HwiErrorCode.DeviceNotReady || deviceEntry.NeedsPinSent is true) - && !pinProvided) + if (deviceEntry.Code is HwiErrorCode.DeviceNotReady && IsTrezorT(deviceEntry)) { - if (!IsTrezorT(deviceEntry)) - { - await websocketHelper.Send("{ \"error\": \"need-pin\"}", cancellationToken); - return true; - } - else - { - try - { - // On trezor T this will prompt the password! (https://github.com/bitcoin-core/HWI/issues/283) - await device.GetXPubAsync(new KeyPath("44'"), cancellationToken); - } - catch (HwiException ex) when (ex.ErrorCode == HwiErrorCode.DeviceAlreadyUnlocked) - { - pinProvided = true; - } - await websocketHelper.Send("{ \"error\": \"need-passphrase-on-device\"}", cancellationToken); - return true; - } + await websocketHelper.Send("{ \"error\": \"need-passphrase-on-device\"}", cancellationToken); + return true; + } + if (deviceEntry.Code is HwiErrorCode.DeviceNotReady || deviceEntry.NeedsPinSent is true) + { + await websocketHelper.Send("{ \"error\": \"need-pin\"}", cancellationToken); + return true; } if ((deviceEntry.Code is HwiErrorCode.DeviceNotReady || deviceEntry.NeedsPassphraseSent is true) && password == null) { - if (IsTrezorT(deviceEntry)) - { - await websocketHelper.Send("{ \"error\": \"need-passphrase-on-device\"}", cancellationToken); - } - else - { - await websocketHelper.Send("{ \"error\": \"need-passphrase\"}", cancellationToken); - } + await websocketHelper.Send("{ \"error\": \"need-passphrase\"}", cancellationToken); return true; } return false; @@ -198,7 +177,6 @@ namespace BTCPayServer.Controllers } catch (HwiException ex) when (ex.ErrorCode == HwiErrorCode.DeviceAlreadyUnlocked) { - pinProvided = true; await websocketHelper.Send("{ \"error\": \"device-already-unlocked\"}", cancellationToken); continue; } @@ -206,7 +184,6 @@ namespace BTCPayServer.Controllers var pin = int.Parse(await websocketHelper.NextMessageAsync(cancellationToken), CultureInfo.InvariantCulture); if (await device.SendPinAsync(pin, cancellationToken)) { - pinProvided = true; await websocketHelper.Send("{ \"info\": \"the pin is correct\"}", cancellationToken); } else @@ -279,15 +256,23 @@ namespace BTCPayServer.Controllers result.Add(new JProperty("keyPath", keyPath.ToString())); await websocketHelper.Send(result.ToString(), cancellationToken); break; - case "refresh-device": + case "ask-passphrase": case "ask-device": - DeviceSelector deviceSelector = (command == "refresh-device" && deviceEntry != null ? deviceEntry.DeviceSelector : null); + if (command == "ask-passphrase") + { + if (deviceEntry == null) + { + await websocketHelper.Send("{ \"error\": \"need-device\"}", cancellationToken); + continue; + } + // The make the trezor T ask for password + await device.GetXPubAsync(new KeyPath("44'"), cancellationToken); + } password = null; - pinProvided = false; deviceEntry = null; device = null; var entries = (await hwi.EnumerateEntriesAsync(cancellationToken)).ToList(); - deviceEntry = entries.Where(h => deviceSelector == null || SameSelector(deviceSelector, h.DeviceSelector)).FirstOrDefault(); + deviceEntry = entries.FirstOrDefault(); if (deviceEntry == null) { await websocketHelper.Send("{ \"error\": \"no-device\"}", cancellationToken); diff --git a/BTCPayServer/wwwroot/js/vaultbridge.ui.js b/BTCPayServer/wwwroot/js/vaultbridge.ui.js index 3ed2e8981..e826cfcb8 100644 --- a/BTCPayServer/wwwroot/js/vaultbridge.ui.js +++ b/BTCPayServer/wwwroot/js/vaultbridge.ui.js @@ -42,7 +42,6 @@ var vaultui = (function () { unexpectedError: new VaultFeedback("failed", "An unexpected error happened. ({{0}})", "vault-feedback3", "unknown-error"), invalidNetwork: new VaultFeedback("failed", "The device is targetting a different chain.", "vault-feedback3", "invalid-network"), needPin: new VaultFeedback("?", "Enter the pin.", "vault-feedback3", "need-pin"), - needPinOnDevice: new VaultFeedback("?", "Please, enter the pin on the device.", "vault-feedback3", "need-pin-on-device"), incorrectPin: new VaultFeedback("failed", "Incorrect pin code.", "vault-feedback3", "incorrect-pin"), invalidPasswordConfirmation: new VaultFeedback("failed", "Invalid password confirmation.", "vault-feedback3", "invalid-password-confirm"), wrongWallet: new VaultFeedback("failed", "This device can't sign the transaction. (Wrong device, wrong passphrase or wrong device fingerprint in your wallet settings)", "vault-feedback3", "wrong-wallet"), @@ -130,15 +129,10 @@ var vaultui = (function () { if (await self.askForPassphrase()) return true; } - if (json.error === "need-pin-on-device" || json.error === "need-passphrase-on-device") { + if (json.error === "need-passphrase-on-device") { handled = true; - if (json.error === "need-pin-on-device") { - show(VaultFeedbacks.needPinOnDevice); - } else { - show(VaultFeedbacks.needPassphraseOnDevice); - } - await self.waitClickContinue(); - self.bridge.socket.send("refresh-device"); + show(VaultFeedbacks.needPassphraseOnDevice); + self.bridge.socket.send("ask-passphrase"); var json = await self.bridge.waitBackendMessage(); if (json.hasOwnProperty("error")) { showError(json); @@ -240,23 +234,6 @@ var vaultui = (function () { }); }; - - /** - * @returns {Promise} - */ - this.waitClickContinue = function () { - $("#vault-confirm").css("display", "block"); - $("#vault-confirm").text("Continue"); - return new Promise(function (resolve, reject) { - $("#vault-confirm").click(async function (e) { - e.preventDefault(); - $("#vault-confirm").css("display", "none"); - $(this).unbind(); - resolve(""); - }); - }); - }; - /** * @returns {Promise} */ From de2e222ae5bfe56a9229f1868854bd01705f57ac Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 10 Dec 2019 18:58:12 +0900 Subject: [PATCH 009/810] Improve signing vault UI --- .../Views/Wallets/WalletSendVault.cshtml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/BTCPayServer/Views/Wallets/WalletSendVault.cshtml b/BTCPayServer/Views/Wallets/WalletSendVault.cshtml index 27536df33..31f868e98 100644 --- a/BTCPayServer/Views/Wallets/WalletSendVault.cshtml +++ b/BTCPayServer/Views/Wallets/WalletSendVault.cshtml @@ -29,7 +29,7 @@ } From 93f490f570c092cada3e7539995e016eea1191bd Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 10 Dec 2019 20:10:13 +0900 Subject: [PATCH 010/810] Fix pin entry --- BTCPayServer/Controllers/VaultController.cs | 39 ++++++++++++--------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/BTCPayServer/Controllers/VaultController.cs b/BTCPayServer/Controllers/VaultController.cs index e85b2c697..f2aa24328 100644 --- a/BTCPayServer/Controllers/VaultController.cs +++ b/BTCPayServer/Controllers/VaultController.cs @@ -78,20 +78,23 @@ namespace BTCPayServer.Controllers await websocketHelper.Send("{ \"error\": \"need-initialized\"}", cancellationToken); return true; } - if (deviceEntry.Code is HwiErrorCode.DeviceNotReady && IsTrezorT(deviceEntry)) + if (deviceEntry.Code is HwiErrorCode.DeviceNotReady) { - await websocketHelper.Send("{ \"error\": \"need-passphrase-on-device\"}", cancellationToken); - return true; - } - if (deviceEntry.Code is HwiErrorCode.DeviceNotReady || deviceEntry.NeedsPinSent is true) - { - await websocketHelper.Send("{ \"error\": \"need-pin\"}", cancellationToken); - return true; - } - if ((deviceEntry.Code is HwiErrorCode.DeviceNotReady || deviceEntry.NeedsPassphraseSent is true) && password == null) - { - await websocketHelper.Send("{ \"error\": \"need-passphrase\"}", cancellationToken); - return true; + if (IsTrezorT(deviceEntry)) + { + await websocketHelper.Send("{ \"error\": \"need-passphrase-on-device\"}", cancellationToken); + return true; + } + else if (deviceEntry.NeedsPinSent is true) + { + await websocketHelper.Send("{ \"error\": \"need-pin\"}", cancellationToken); + return true; + } + else if (deviceEntry.NeedsPassphraseSent is true && password is null) + { + await websocketHelper.Send("{ \"error\": \"need-passphrase\"}", cancellationToken); + return true; + } } return false; } @@ -184,14 +187,13 @@ namespace BTCPayServer.Controllers var pin = int.Parse(await websocketHelper.NextMessageAsync(cancellationToken), CultureInfo.InvariantCulture); if (await device.SendPinAsync(pin, cancellationToken)) { - await websocketHelper.Send("{ \"info\": \"the pin is correct\"}", cancellationToken); + goto askdevice; } else { await websocketHelper.Send("{ \"error\": \"incorrect-pin\"}", cancellationToken); continue; } - break; case "ask-xpub": if (await RequireDeviceUnlocking()) { @@ -257,7 +259,6 @@ namespace BTCPayServer.Controllers await websocketHelper.Send(result.ToString(), cancellationToken); break; case "ask-passphrase": - case "ask-device": if (command == "ask-passphrase") { if (deviceEntry == null) @@ -268,11 +269,15 @@ namespace BTCPayServer.Controllers // The make the trezor T ask for password await device.GetXPubAsync(new KeyPath("44'"), cancellationToken); } + goto askdevice; + case "ask-device": +askdevice: + var selector = deviceEntry?.DeviceSelector; password = null; deviceEntry = null; device = null; var entries = (await hwi.EnumerateEntriesAsync(cancellationToken)).ToList(); - deviceEntry = entries.FirstOrDefault(); + deviceEntry = entries.Where(e => selector == null || SameSelector(selector, e.DeviceSelector)).FirstOrDefault(); if (deviceEntry == null) { await websocketHelper.Send("{ \"error\": \"no-device\"}", cancellationToken); From 540cb912f3982f64e8f1a770aa36690a93cbd00c Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 10 Dec 2019 21:22:46 +0900 Subject: [PATCH 011/810] Can display address on device at confirmation screen --- .../Controllers/StoresController.BTCLike.cs | 4 +- BTCPayServer/Controllers/VaultController.cs | 21 ++++++++ .../DerivationSchemeViewModel.cs | 15 +++++- .../Views/Stores/AddDerivationScheme.cshtml | 44 ++++++++++++++- ...vationSchemes_HardwareWalletDialogs.cshtml | 6 --- .../wwwroot/js/StoreAddDerivationScheme.js | 54 +++++++++++++------ BTCPayServer/wwwroot/js/vaultbridge.ui.js | 15 ++++++ 7 files changed, 132 insertions(+), 27 deletions(-) diff --git a/BTCPayServer/Controllers/StoresController.BTCLike.cs b/BTCPayServer/Controllers/StoresController.BTCLike.cs index cf4e6c92b..23082fb37 100644 --- a/BTCPayServer/Controllers/StoresController.BTCLike.cs +++ b/BTCPayServer/Controllers/StoresController.BTCLike.cs @@ -337,8 +337,10 @@ namespace BTCPayServer.Controllers for (int i = 0; i < 10; i++) { + var keyPath = deposit.GetKeyPath((uint)i); + var rootedKeyPath = vm.GetAccountKeypath()?.Derive(keyPath); var address = line.Derive((uint)i); - vm.AddressSamples.Add((deposit.GetKeyPath((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(strategy.Network.NBitcoinNetwork).ToString())); + vm.AddressSamples.Add((keyPath.ToString(), address.ScriptPubKey.GetDestinationAddress(strategy.Network.NBitcoinNetwork).ToString(), rootedKeyPath)); } } vm.Confirmation = true; diff --git a/BTCPayServer/Controllers/VaultController.cs b/BTCPayServer/Controllers/VaultController.cs index f2aa24328..27821464f 100644 --- a/BTCPayServer/Controllers/VaultController.cs +++ b/BTCPayServer/Controllers/VaultController.cs @@ -168,6 +168,15 @@ namespace BTCPayServer.Controllers o.Add("psbt", psbt.ToBase64()); await websocketHelper.Send(o.ToString(), cancellationToken); break; + case "display-address": + if (await RequireDeviceUnlocking()) + { + continue; + } + var k = RootedKeyPath.Parse(await websocketHelper.NextMessageAsync(cancellationToken)); + await device.DisplayAddressAsync(GetScriptPubKeyType(k), k.KeyPath, cancellationToken); + await websocketHelper.Send("{ \"info\": \"ok\"}", cancellationToken); + break; case "ask-pin": if (device == null) { @@ -324,6 +333,18 @@ askdevice: return new EmptyResult(); } + private ScriptPubKeyType GetScriptPubKeyType(RootedKeyPath keyPath) + { + var path = keyPath.KeyPath.ToString(); + if (path.StartsWith("84'", StringComparison.OrdinalIgnoreCase)) + return ScriptPubKeyType.Segwit; + if (path.StartsWith("49'", StringComparison.OrdinalIgnoreCase)) + return ScriptPubKeyType.SegwitP2SH; + if (path.StartsWith("44'", StringComparison.OrdinalIgnoreCase)) + return ScriptPubKeyType.Legacy; + throw new NotSupportedException("Unsupported keypath"); + } + private bool SameSelector(DeviceSelector a, DeviceSelector b) { var aargs = new List(); diff --git a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs index 1768eb151..474d03dd6 100644 --- a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs @@ -19,10 +19,10 @@ namespace BTCPayServer.Models.StoreViewModels get; set; } - public List<(string KeyPath, string Address)> AddressSamples + public List<(string KeyPath, string Address, RootedKeyPath RootedKeyPath)> AddressSamples { get; set; - } = new List<(string KeyPath, string Address)>(); + } = new List<(string KeyPath, string Address, RootedKeyPath RootedKeyPath)>(); public string CryptoCode { get; set; } public string KeyPath { get; set; } @@ -41,5 +41,16 @@ namespace BTCPayServer.Models.StoreViewModels public string DerivationSchemeFormat { get; set; } public string AccountKey { get; set; } public BTCPayNetwork Network { get; set; } + + public RootedKeyPath GetAccountKeypath() + { + if (KeyPath != null && RootFingerprint != null && + NBitcoin.KeyPath.TryParse(KeyPath, out var p) && + HDFingerprint.TryParse(RootFingerprint, out var fp)) + { + return new RootedKeyPath(fp, p); + } + return null; + } } } diff --git a/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml b/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml index e62478de9..24b32dd3e 100644 --- a/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml +++ b/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml @@ -6,6 +6,7 @@ @section HeadScripts { +} diff --git a/BTCPayServer/Views/Wallets/WalletsNavPages.cs b/BTCPayServer/Views/Wallets/WalletsNavPages.cs index 981c23df3..d7412a65c 100644 --- a/BTCPayServer/Views/Wallets/WalletsNavPages.cs +++ b/BTCPayServer/Views/Wallets/WalletsNavPages.cs @@ -11,6 +11,7 @@ namespace BTCPayServer.Views.Wallets Transactions, Rescan, PSBT, - Settings + Settings, + Receive } } diff --git a/BTCPayServer/Views/Wallets/_Nav.cshtml b/BTCPayServer/Views/Wallets/_Nav.cshtml index 9df785349..dc77bc265 100644 --- a/BTCPayServer/Views/Wallets/_Nav.cshtml +++ b/BTCPayServer/Views/Wallets/_Nav.cshtml @@ -10,6 +10,7 @@ { Send } + Receive Rescan @if (!network.ReadonlyWallet) { From a2be7ee47185fe4270406f387a1acf5857a29e43 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sat, 18 Jan 2020 15:32:01 +0900 Subject: [PATCH 124/810] Fix statusMessage handling for the Receive wallet page --- BTCPayServer/Controllers/WalletsController.cs | 16 ++-- .../Views/Wallets/WalletReceive.cshtml | 78 ++++++++++--------- 2 files changed, 48 insertions(+), 46 deletions(-) diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index 921cc45f7..cfe1a14ff 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -307,7 +307,7 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("{walletId}/receive")] public IActionResult WalletReceive([ModelBinder(typeof(WalletIdModelBinder))] - WalletId walletId, string statusMessage = null) + WalletId walletId) { if (walletId?.StoreId == null) return NotFound(); @@ -319,11 +319,6 @@ namespace BTCPayServer.Controllers return NotFound(); var address = _WalletReceiveStateService.Get(walletId)?.Address; - if (!string.IsNullOrEmpty(statusMessage)) - { - TempData[WellKnownTempData.SuccessMessage] = statusMessage; - } - return View(new WalletReceiveViewModel() { CryptoCode = walletId.CryptoCode, @@ -345,7 +340,6 @@ namespace BTCPayServer.Controllers var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode); if (network == null) return NotFound(); - var statusMessage = string.Empty; var wallet = _walletProvider.GetWallet(network); switch (command) { @@ -358,12 +352,12 @@ namespace BTCPayServer.Controllers var address = cachedAddress.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork); ExplorerClientProvider.GetExplorerClient(network) .CancelReservation(cachedAddress.DerivationStrategy, new[] {cachedAddress.KeyPath}); - statusMessage = new StatusMessageModel() + this.TempData.SetStatusMessageModel(new StatusMessageModel() { - AllowDismiss =true, + AllowDismiss = true, Message = $"Address {address} was unreserved.", Severity = StatusMessageModel.StatusSeverity.Success, - }.ToString(); + }); _WalletReceiveStateService.Remove(walletId); break; case "generate-new-address": @@ -371,7 +365,7 @@ namespace BTCPayServer.Controllers _WalletReceiveStateService.Set(walletId, reserve); break; } - return RedirectToAction(nameof(WalletReceive), new {walletId, statusMessage}); + return RedirectToAction(nameof(WalletReceive), new {walletId}); } [HttpGet] diff --git a/BTCPayServer/Views/Wallets/WalletReceive.cshtml b/BTCPayServer/Views/Wallets/WalletReceive.cshtml index 8caa19db4..44e3a5c55 100644 --- a/BTCPayServer/Views/Wallets/WalletReceive.cshtml +++ b/BTCPayServer/Views/Wallets/WalletReceive.cshtml @@ -5,13 +5,20 @@ ViewData["Title"] = "Manage wallet"; ViewData.SetActivePageAndTitle(WalletsNavPages.Receive); } - +@if (TempData.HasStatusMessage()) +{ +
+
+ +
+
+}
@if (string.IsNullOrEmpty(Model.Address)) { - +

Receive @Model.CryptoCode

} @@ -21,14 +28,14 @@
@@ -38,17 +45,17 @@
- +
- - + +
} - +
@@ -56,7 +63,7 @@ @section HeadScripts { - + - + + .copy { + cursor: copy; + } + } From 4b088defd3cf1c3f7c7501073a722202beb69bbb Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sat, 18 Jan 2020 15:36:20 +0900 Subject: [PATCH 125/810] Increase timeout for AssertHappyMessage test --- BTCPayServer.Tests/SeleniumTester.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index e0aee0093..c7f163312 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -72,7 +72,7 @@ namespace BTCPayServer.Tests internal void AssertHappyMessage() { - using var cts = new CancellationTokenSource(10_000); + using var cts = new CancellationTokenSource(20_000); while (!cts.IsCancellationRequested) { var success = Driver.FindElements(By.ClassName("alert-success")).Where(el => el.Displayed).Any(); From 1bf04ac4acf516821b41a2ca8e79f8e5b4b0a16a Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sat, 18 Jan 2020 16:20:03 +0900 Subject: [PATCH 126/810] Generate less keys with nbxplorer --- BTCPayServer.Tests/docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index 4c5564952..fcf1ce112 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -98,6 +98,8 @@ services: NBXPLORER_LBTCRPCUSER: "liquid" NBXPLORER_LBTCRPCPASSWORD: "liquid" NBXPLORER_BIND: 0.0.0.0:32838 + NBXPLORER_MINGAPSIZE: 5 + NBXPLORER_MAXGAPSIZE: 10 NBXPLORER_VERBOSE: 1 NBXPLORER_NOAUTH: 1 links: From 76008c9f5cffcf0735590cbe3f8579e5c8f1586e Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sat, 18 Jan 2020 16:59:25 +0900 Subject: [PATCH 127/810] bump to sdk 3.1.101 and version 3.1.1 (security patch) --- BTCPayServer.Data/BTCPayServer.Data.csproj | 4 ++-- BTCPayServer.Tests/BTCPayServer.Tests.csproj | 2 +- BTCPayServer.Tests/Dockerfile | 2 +- BTCPayServer/BTCPayServer.csproj | 4 ++-- amd64.Dockerfile | 4 ++-- arm32v7.Dockerfile | 4 ++-- arm64v8.Dockerfile | 4 ++-- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/BTCPayServer.Data/BTCPayServer.Data.csproj b/BTCPayServer.Data/BTCPayServer.Data.csproj index e25b7789b..0a1a49cd6 100644 --- a/BTCPayServer.Data/BTCPayServer.Data.csproj +++ b/BTCPayServer.Data/BTCPayServer.Data.csproj @@ -3,10 +3,10 @@ - + - + diff --git a/BTCPayServer.Tests/BTCPayServer.Tests.csproj b/BTCPayServer.Tests/BTCPayServer.Tests.csproj index 2e007f098..68dcbfc74 100644 --- a/BTCPayServer.Tests/BTCPayServer.Tests.csproj +++ b/BTCPayServer.Tests/BTCPayServer.Tests.csproj @@ -25,7 +25,7 @@ - + all diff --git a/BTCPayServer.Tests/Dockerfile b/BTCPayServer.Tests/Dockerfile index beacd0322..ea37b47b6 100644 --- a/BTCPayServer.Tests/Dockerfile +++ b/BTCPayServer.Tests/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/core/sdk:3.1.100 AS builder +FROM mcr.microsoft.com/dotnet/core/sdk:3.1.101 AS builder RUN apt-get update && apt-get install -y --no-install-recommends chromium-driver \ && rm -rf /var/lib/apt/lists/* diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index a158ff379..e2e795e75 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -69,8 +69,8 @@ - - + + diff --git a/amd64.Dockerfile b/amd64.Dockerfile index 5d06c2a4b..ab43bda09 100644 --- a/amd64.Dockerfile +++ b/amd64.Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/core/sdk:3.1.100 AS builder +FROM mcr.microsoft.com/dotnet/core/sdk:3.1.101 AS builder WORKDIR /source COPY nuget.config nuget.config COPY Build/Common.csproj Build/Common.csproj @@ -14,7 +14,7 @@ COPY BTCPayServer/. BTCPayServer/. COPY Build/Version.csproj Build/Version.csproj RUN cd BTCPayServer && dotnet publish --output /app/ --configuration Release -FROM mcr.microsoft.com/dotnet/core/aspnet:3.1.0-buster-slim +FROM mcr.microsoft.com/dotnet/core/aspnet:3.1.1-buster-slim RUN apt-get update && apt-get install -y --no-install-recommends iproute2 openssh-client \ && rm -rf /var/lib/apt/lists/* diff --git a/arm32v7.Dockerfile b/arm32v7.Dockerfile index 10eae21da..5418edf45 100644 --- a/arm32v7.Dockerfile +++ b/arm32v7.Dockerfile @@ -1,5 +1,5 @@ # This is a manifest image, will pull the image with the same arch as the builder machine -FROM mcr.microsoft.com/dotnet/core/sdk:3.1.100 AS builder +FROM mcr.microsoft.com/dotnet/core/sdk:3.1.101 AS builder RUN apt-get update \ && apt-get install -qq --no-install-recommends qemu qemu-user-static qemu-user binfmt-support @@ -19,7 +19,7 @@ COPY Build/Version.csproj Build/Version.csproj RUN cd BTCPayServer && dotnet publish --output /app/ --configuration Release # Force the builder machine to take make an arm runtime image. This is fine as long as the builder does not run any program -FROM mcr.microsoft.com/dotnet/core/aspnet:3.1.0-buster-slim-arm32v7 +FROM mcr.microsoft.com/dotnet/core/aspnet:3.1.1-buster-slim-arm32v7 COPY --from=builder /usr/bin/qemu-arm-static /usr/bin/qemu-arm-static RUN apt-get update && apt-get install -y --no-install-recommends iproute2 openssh-client \ && rm -rf /var/lib/apt/lists/* diff --git a/arm64v8.Dockerfile b/arm64v8.Dockerfile index 6dde4cf44..85d3ee42b 100644 --- a/arm64v8.Dockerfile +++ b/arm64v8.Dockerfile @@ -1,5 +1,5 @@ # This is a manifest image, will pull the image with the same arch as the builder machine -FROM mcr.microsoft.com/dotnet/core/sdk:3.1.100 AS builder +FROM mcr.microsoft.com/dotnet/core/sdk:3.1.101 AS builder RUN apt-get update \ && apt-get install -qq --no-install-recommends qemu qemu-user-static qemu-user binfmt-support @@ -19,7 +19,7 @@ COPY Build/Version.csproj Build/Version.csproj RUN cd BTCPayServer && dotnet publish --output /app/ --configuration Release # Force the builder machine to take make an arm runtime image. This is fine as long as the builder does not run any program -FROM mcr.microsoft.com/dotnet/core/aspnet:3.1.0-buster-slim-arm64v8 +FROM mcr.microsoft.com/dotnet/core/aspnet:3.1.1-buster-slim-arm64v8 COPY --from=builder /usr/bin/qemu-aarch64-static /usr/bin/qemu-aarch64-static RUN apt-get update && apt-get install -y --no-install-recommends iproute2 openssh-client \ && rm -rf /var/lib/apt/lists/* From 1bf680fdb93a1116d2f94d92989bfa50727a84a3 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sat, 18 Jan 2020 19:23:40 +0900 Subject: [PATCH 128/810] Improve tests to not create new HttpClient every times --- BTCPayServer.Tests/ChangellyTests.cs | 12 ++---------- BTCPayServer.Tests/TestUtils.cs | 9 +++++++++ BTCPayServer.Tests/UnitTest1.cs | 7 ++++++- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/BTCPayServer.Tests/ChangellyTests.cs b/BTCPayServer.Tests/ChangellyTests.cs index 16c666a76..9579b1803 100644 --- a/BTCPayServer.Tests/ChangellyTests.cs +++ b/BTCPayServer.Tests/ChangellyTests.cs @@ -182,7 +182,7 @@ namespace BTCPayServer.Tests var factory = UnitTest1.CreateBTCPayRateFactory(); var fetcher = new RateFetcher(factory); - var httpClientFactory = new MockHttpClientFactory(); + var httpClientFactory = TestUtils.CreateHttpFactory(); var changellyController = new ChangellyController( new ChangellyClientProvider(tester.PayTester.StoreRepository, httpClientFactory), tester.NetworkProvider, fetcher); @@ -213,7 +213,7 @@ namespace BTCPayServer.Tests var factory = UnitTest1.CreateBTCPayRateFactory(); var fetcher = new RateFetcher(factory); - var httpClientFactory = new MockHttpClientFactory(); + var httpClientFactory = TestUtils.CreateHttpFactory(); var changellyController = new ChangellyController( new ChangellyClientProvider(tester.PayTester.StoreRepository, httpClientFactory), tester.NetworkProvider, fetcher); @@ -243,12 +243,4 @@ namespace BTCPayServer.Tests Assert.Equal(20, ChangellyCalculationHelper.ComputeCorrectAmount(10, 1, 2)); } } - - public class MockHttpClientFactory : IHttpClientFactory - { - public HttpClient CreateClient(string name) - { - return new HttpClient(); - } - } } diff --git a/BTCPayServer.Tests/TestUtils.cs b/BTCPayServer.Tests/TestUtils.cs index f0b365e6b..c76d6131a 100644 --- a/BTCPayServer.Tests/TestUtils.cs +++ b/BTCPayServer.Tests/TestUtils.cs @@ -7,6 +7,8 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Xunit.Sdk; using System.Linq; +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; namespace BTCPayServer.Tests { @@ -104,5 +106,12 @@ namespace BTCPayServer.Tests } } } + + internal static IHttpClientFactory CreateHttpFactory() + { + var services = new ServiceCollection(); + services.AddHttpClient(); + return services.BuildServiceProvider().GetRequiredService(); + } } } diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index e2dae7e36..a3b8174d6 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -2681,6 +2681,7 @@ noninventoryitem: public void CanQueryDirectProviders() { var factory = CreateBTCPayRateFactory(); + var directlySupported = factory.GetSupportedExchanges().Where(s => s.Source == RateSource.Direct).Select(s => s.Id).ToHashSet(); var all = string.Join("\r\n", factory.GetSupportedExchanges().Select(e => e.Id).ToArray()); foreach (var result in factory .Providers @@ -2712,6 +2713,10 @@ noninventoryitem: && e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD ); } + // We are not showing a directly implemented exchange as directly implemented in the UI + // we need to modify the AvailableRateProvider + if (result.ExpectedName != "coinaverage") + Assert.Contains(result.ExpectedName, directlySupported); } // Kraken emit one request only after first GetRates factory.Providers["kraken"].GetRatesAsync(default).GetAwaiter().GetResult(); @@ -2782,7 +2787,7 @@ noninventoryitem: public static RateProviderFactory CreateBTCPayRateFactory() { - return new RateProviderFactory(new MockHttpClientFactory()); + return new RateProviderFactory(TestUtils.CreateHttpFactory()); } class SpyRateProvider : IRateProvider From 25b733ca7f11c55a12673ee2d46222761b1a3ae8 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sat, 18 Jan 2020 19:42:46 +0900 Subject: [PATCH 129/810] Remove knowledge of ExchangeName from BackgroundRateFetcher --- .../BackgroundFetcherRateProvider.cs | 9 +--- .../Providers/KrakenExchangeRateProvider.cs | 2 - .../Services/RateProviderFactory.cs | 7 +-- BTCPayServer.Tests/UnitTest1.cs | 5 +- .../HostedServices/RatesHostedService.cs | 48 +++++++++++-------- 5 files changed, 36 insertions(+), 35 deletions(-) diff --git a/BTCPayServer.Rating/Providers/BackgroundFetcherRateProvider.cs b/BTCPayServer.Rating/Providers/BackgroundFetcherRateProvider.cs index b74756e0d..da779b195 100644 --- a/BTCPayServer.Rating/Providers/BackgroundFetcherRateProvider.cs +++ b/BTCPayServer.Rating/Providers/BackgroundFetcherRateProvider.cs @@ -86,21 +86,17 @@ namespace BTCPayServer.Services.Rates IRateProvider _Inner; public IRateProvider Inner => _Inner; - public BackgroundFetcherRateProvider(string exchangeName, IRateProvider inner) + public BackgroundFetcherRateProvider(IRateProvider inner) { if (inner == null) throw new ArgumentNullException(nameof(inner)); - if (exchangeName == null) - throw new ArgumentNullException(nameof(exchangeName)); _Inner = inner; - ExchangeName = exchangeName; } public BackgroundFetcherState GetState() { var state = new BackgroundFetcherState() { - ExchangeName = ExchangeName, LastRequested = LastRequested }; if (_Latest is LatestFetch fetch) @@ -118,8 +114,6 @@ namespace BTCPayServer.Services.Rates public void LoadState(BackgroundFetcherState state) { - if (ExchangeName != state.ExchangeName) - throw new InvalidOperationException("The state does not belong to this fetcher"); if (state.LastRequested is DateTimeOffset lastRequested) this.LastRequested = state.LastRequested; if (state.LastUpdated is DateTimeOffset updated && state.Rates is List rates) @@ -220,7 +214,6 @@ namespace BTCPayServer.Services.Rates /// public DateTimeOffset? LastRequested { get; set; } - public string ExchangeName { get; } public DateTimeOffset? Expiration { get diff --git a/BTCPayServer.Rating/Providers/KrakenExchangeRateProvider.cs b/BTCPayServer.Rating/Providers/KrakenExchangeRateProvider.cs index 135705047..7bba26c06 100644 --- a/BTCPayServer.Rating/Providers/KrakenExchangeRateProvider.cs +++ b/BTCPayServer.Rating/Providers/KrakenExchangeRateProvider.cs @@ -33,8 +33,6 @@ namespace BTCPayServer.Services.Rates } } - public string ExchangeName => "kraken"; - HttpClient _LocalClient; static HttpClient _Client = new HttpClient(); diff --git a/BTCPayServer.Rating/Services/RateProviderFactory.cs b/BTCPayServer.Rating/Services/RateProviderFactory.cs index 4c2f0809b..1a3de5e00 100644 --- a/BTCPayServer.Rating/Services/RateProviderFactory.cs +++ b/BTCPayServer.Rating/Services/RateProviderFactory.cs @@ -64,6 +64,7 @@ namespace BTCPayServer.Services.Rates return _DirectProviders; } } + internal IEnumerable GetDirectlySupportedExchanges() { yield return new AvailableRateProvider("binance", "Binance", "https://api.binance.com/api/v1/ticker/24hr"); @@ -106,7 +107,7 @@ namespace BTCPayServer.Services.Rates foreach (var provider in Providers.ToArray()) { - var prov = new BackgroundFetcherRateProvider(provider.Key, Providers[provider.Key]); + var prov = new BackgroundFetcherRateProvider(Providers[provider.Key]); prov.RefreshRate = TimeSpan.FromMinutes(1.0); prov.ValidatyTime = TimeSpan.FromMinutes(5.0); Providers[provider.Key] = prov; @@ -118,9 +119,9 @@ namespace BTCPayServer.Services.Rates { var coingecko = new CoinGeckoRateProvider(_httpClientFactory) { - UnderlyingExchange = supportedExchange.Id + UnderlyingExchange = supportedExchange.SourceId }; - var bgFetcher = new BackgroundFetcherRateProvider(supportedExchange.Id, coingecko); + var bgFetcher = new BackgroundFetcherRateProvider(coingecko); bgFetcher.RefreshRate = TimeSpan.FromMinutes(1.0); bgFetcher.ValidatyTime = TimeSpan.FromMinutes(5.0); Providers.Add(supportedExchange.Id, bgFetcher); diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index a3b8174d6..b459e51ea 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -2732,7 +2732,7 @@ noninventoryitem: await provider.GetRatesAsync(default); var state = provider.GetState(); Assert.Single(state.Rates, r => r.Pair == new CurrencyPair("BTC", "EUR")); - var provider2 = new BackgroundFetcherRateProvider("kraken", provider.Inner) + var provider2 = new BackgroundFetcherRateProvider(provider.Inner) { RefreshRate = provider.RefreshRate, ValidatyTime = provider.ValidatyTime @@ -2751,7 +2751,6 @@ noninventoryitem: // Should not throw, as things should be cached await provider2.GetRatesAsync(cts.Token); } - Assert.Equal(provider.ExchangeName, provider2.ExchangeName); Assert.Equal(provider.NextUpdate, provider2.NextUpdate); Assert.NotEqual(provider.LastRequested, provider2.LastRequested); Assert.Equal(provider.Expiration, provider2.Expiration); @@ -2897,7 +2896,7 @@ noninventoryitem: factory.Providers.Clear(); var fetcher = new RateFetcher(factory); factory.Providers.Clear(); - var fetch = new BackgroundFetcherRateProvider("spy", spy); + var fetch = new BackgroundFetcherRateProvider(spy); fetch.DoNotAutoFetchIfExpired = true; factory.Providers.Add("bittrex", fetch); var fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult(); diff --git a/BTCPayServer/HostedServices/RatesHostedService.cs b/BTCPayServer/HostedServices/RatesHostedService.cs index 801ce9a09..cf6fb1b62 100644 --- a/BTCPayServer/HostedServices/RatesHostedService.cs +++ b/BTCPayServer/HostedServices/RatesHostedService.cs @@ -50,13 +50,20 @@ namespace BTCPayServer.HostedServices return fetcher.LastRequested is DateTimeOffset v && DateTimeOffset.UtcNow - v < TimeSpan.FromDays(1.0); } + + IEnumerable<(string ExchangeName, BackgroundFetcherRateProvider Fetcher)> GetStillUsedProviders() + { + foreach (var kv in _RateProviderFactory.Providers) + { + if (kv.Value is BackgroundFetcherRateProvider fetcher && IsStillUsed(fetcher)) + { + yield return (kv.Key, fetcher); + } + } + } async Task RefreshRates() { - var usedProviders = _RateProviderFactory.Providers - .Select(p => p.Value) - .OfType() - .Where(IsStillUsed) - .ToArray(); + var usedProviders = GetStillUsedProviders().ToArray(); if (usedProviders.Length == 0) { await Task.Delay(TimeSpan.FromSeconds(30), Cancellation); @@ -68,11 +75,11 @@ namespace BTCPayServer.HostedServices try { await Task.WhenAll(usedProviders - .Select(p => p.UpdateIfNecessary(timeout.Token).ContinueWith(t => + .Select(p => p.Fetcher.UpdateIfNecessary(timeout.Token).ContinueWith(t => { if (t.Result.Exception != null) { - Logs.PayServer.LogWarning($"Error while contacting {p.ExchangeName}: {t.Result.Exception.Message}"); + Logs.PayServer.LogWarning($"Error while contacting exchange {p.ExchangeName}: {t.Result.Exception.Message}"); } }, TaskScheduler.Default)) .ToArray()).WithCancellation(timeout.Token); @@ -113,13 +120,13 @@ namespace BTCPayServer.HostedServices { _LastCacheDate = cache.Created; var stateByExchange = cache.States.ToDictionary(o => o.ExchangeName); - foreach (var obj in _RateProviderFactory.Providers - .Select(p => p.Value) - .OfType() - .Select(v => (Fetcher: v, State: stateByExchange.TryGet(v.ExchangeName))) - .Where(v => v.State != null)) + foreach (var provider in _RateProviderFactory.Providers) { - obj.Fetcher.LoadState(obj.State); + if (stateByExchange.TryGetValue(provider.Key, out var state) && + provider.Value is BackgroundFetcherRateProvider fetcher) + { + fetcher.LoadState(state); + } } } } @@ -130,12 +137,15 @@ namespace BTCPayServer.HostedServices var cache = new ExchangeRatesCache(); cache.Created = DateTimeOffset.UtcNow; _LastCacheDate = cache.Created; - cache.States = _RateProviderFactory.Providers - .Select(p => p.Value) - .OfType() - .Where(IsStillUsed) - .Select(p => p.GetState()) - .ToList(); + + var usedProviders = GetStillUsedProviders().ToArray(); + cache.States = new List(usedProviders.Length); + foreach (var provider in usedProviders) + { + var state = provider.Fetcher.GetState(); + state.ExchangeName = provider.ExchangeName; + cache.States.Add(state); + } await _SettingsRepository.UpdateSetting(cache); } } From 53a60d1660db9fd960dc759a2ccf5132295ad1f1 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sat, 18 Jan 2020 21:48:04 +0900 Subject: [PATCH 130/810] Add Direct Provider with bitfinex, okex and coinbasepro --- .../BTCPayServer.Rating.csproj | 4 - .../Providers/ExchangeSharpRateProvider.cs | 35 +-- .../Providers/HttpClientRequestMaker.cs | 214 ++++++++++++++++++ .../Services/RateProviderFactory.cs | 37 ++- BTCPayServer.Tests/UnitTest1.cs | 4 +- 5 files changed, 261 insertions(+), 33 deletions(-) create mode 100644 BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs diff --git a/BTCPayServer.Rating/BTCPayServer.Rating.csproj b/BTCPayServer.Rating/BTCPayServer.Rating.csproj index 002492952..a3085dc01 100644 --- a/BTCPayServer.Rating/BTCPayServer.Rating.csproj +++ b/BTCPayServer.Rating/BTCPayServer.Rating.csproj @@ -2,10 +2,6 @@ - - - - diff --git a/BTCPayServer.Rating/Providers/ExchangeSharpRateProvider.cs b/BTCPayServer.Rating/Providers/ExchangeSharpRateProvider.cs index b34f89e94..6fe4c212d 100644 --- a/BTCPayServer.Rating/Providers/ExchangeSharpRateProvider.cs +++ b/BTCPayServer.Rating/Providers/ExchangeSharpRateProvider.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Rating; @@ -11,16 +12,15 @@ using ExchangeSharp; namespace BTCPayServer.Services.Rates { - public class ExchangeSharpRateProvider : IRateProvider + public class ExchangeSharpRateProvider : IRateProvider where T : ExchangeAPI, new() { - readonly ExchangeAPI _ExchangeAPI; - public ExchangeSharpRateProvider(ExchangeAPI exchangeAPI, bool reverseCurrencyPair = false) + HttpClient _httpClient; + public ExchangeSharpRateProvider(HttpClient httpClient, bool reverseCurrencyPair = false) { - if (exchangeAPI == null) - throw new ArgumentNullException(nameof(exchangeAPI)); - exchangeAPI.RequestTimeout = TimeSpan.FromSeconds(5.0); - _ExchangeAPI = exchangeAPI; + if (httpClient == null) + throw new ArgumentNullException(nameof(httpClient)); ReverseCurrencyPair = reverseCurrencyPair; + _httpClient = httpClient; } public bool ReverseCurrencyPair @@ -31,14 +31,17 @@ namespace BTCPayServer.Services.Rates public async Task GetRatesAsync(CancellationToken cancellationToken) { await new SynchronizationContextRemover(); - var rates = await _ExchangeAPI.GetTickersAsync(); - var exchangeRateTasks = rates - .Where(t => t.Value.Ask != 0m && t.Value.Bid != 0m) - .Select(t => CreateExchangeRate(t)); + var exchangeAPI = new T(); + exchangeAPI.RequestMaker = new HttpClientRequestMaker(exchangeAPI, _httpClient, cancellationToken); + var rates = await exchangeAPI.GetTickersAsync(); + + var exchangeRateTasks = rates + .Where(t => t.Value.Ask != 0m && t.Value.Bid != 0m) + .Select(t => CreateExchangeRate(exchangeAPI, t)); + + var exchangeRates = await Task.WhenAll(exchangeRateTasks); - var exchangeRates = await Task.WhenAll(exchangeRateTasks); - return exchangeRates .Where(t => t != null) .ToArray(); @@ -46,19 +49,19 @@ namespace BTCPayServer.Services.Rates // ExchangeSymbolToGlobalSymbol throws exception which would kill perf ConcurrentDictionary notFoundSymbols = new ConcurrentDictionary(); - private async Task CreateExchangeRate(KeyValuePair ticker) + private async Task CreateExchangeRate(T exchangeAPI, KeyValuePair ticker) { if (notFoundSymbols.TryGetValue(ticker.Key, out _)) return null; try { - var tickerName = await _ExchangeAPI.ExchangeMarketSymbolToGlobalMarketSymbolAsync(ticker.Key); + var tickerName = await exchangeAPI.ExchangeMarketSymbolToGlobalMarketSymbolAsync(ticker.Key); if (!CurrencyPair.TryParse(tickerName, out var pair)) { notFoundSymbols.TryAdd(ticker.Key, ticker.Key); return null; } - if(ReverseCurrencyPair) + if (ReverseCurrencyPair) pair = new CurrencyPair(pair.Right, pair.Left); return new PairRate(pair, new BidAsk(ticker.Value.Bid, ticker.Value.Ask)); } diff --git a/BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs b/BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs new file mode 100644 index 000000000..17fccf327 --- /dev/null +++ b/BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs @@ -0,0 +1,214 @@ +using System; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using ExchangeSharp; +using System.Threading; + +namespace BTCPayServer.Services.Rates +{ + internal class HttpClientRequestMaker : IAPIRequestMaker + { + class InternalHttpWebRequest : IHttpWebRequest + { + internal readonly HttpWebRequest Request; + + public Uri RequestUri => Request.RequestUri; + + public string Method + { + get + { + return Request.Method; + } + set + { + Request.Method = value; + } + } + + public int Timeout + { + get + { + return Request.Timeout; + } + set + { + Request.Timeout = value; + } + } + + public int ReadWriteTimeout + { + get + { + return Request.ReadWriteTimeout; + } + set + { + Request.ReadWriteTimeout = value; + } + } + + public InternalHttpWebRequest(Uri fullUri) + { + Request = ((WebRequest.Create(fullUri) as HttpWebRequest) ?? throw new NullReferenceException("Failed to create HttpWebRequest")); + Request.KeepAlive = false; + } + + public void AddHeader(string header, string value) + { + switch (header.ToStringLowerInvariant()) + { + case "content-type": + Request.ContentType = value; + break; + case "content-length": + Request.ContentLength = value.ConvertInvariant(0L); + break; + case "user-agent": + Request.UserAgent = value; + break; + case "accept": + Request.Accept = value; + break; + case "connection": + Request.Connection = value; + break; + default: + Request.Headers[header] = value; + break; + } + } + + public Task WriteAllAsync(byte[] data, int index, int length) + { + throw new NotImplementedException(); + } + + public HttpRequestMessage ToHttpRequestMessage() + { + var httpRequest = new HttpRequestMessage(HttpMethod.Get, Request.RequestUri); + CopyHeadersFrom(httpRequest, Request); + return httpRequest; + } + + internal void CopyHeadersFrom(HttpRequestMessage message, HttpWebRequest request) + { + foreach (string headerName in request.Headers) + { + string[] headerValues = request.Headers.GetValues(headerName); + if (!message.Headers.TryAddWithoutValidation(headerName, headerValues)) + { + if (message.Content != null) + message.Content.Headers.TryAddWithoutValidation(headerName, headerValues); + } + } + } + } + class InternalHttpWebResponse : IHttpWebResponse + { + public InternalHttpWebResponse(HttpResponseMessage httpResponseMessage) + { + var headers = new Dictionary>(); + foreach (var h in httpResponseMessage.Headers) + { + if (!headers.TryGetValue(h.Key, out var list)) + { + list = new List(); + headers.Add(h.Key, list); + } + list.AddRange(h.Value); + } + Headers = new Dictionary>(headers.Count); + foreach (var item in headers) + { + Headers.Add(item.Key, item.Value.AsReadOnly()); + } + } + public Dictionary> Headers { get; } + static IReadOnlyList Empty = new List().AsReadOnly(); + public IReadOnlyList GetHeader(string name) + { + Headers.TryGetValue(name, out var list); + return list ?? Empty; + } + } + private readonly IAPIRequestHandler api; + private readonly HttpClient _httpClient; + private readonly CancellationToken _cancellationToken; + + public HttpClientRequestMaker(IAPIRequestHandler api, HttpClient httpClient, CancellationToken cancellationToken) + { + if (api == null) + throw new ArgumentNullException(nameof(api)); + if (httpClient == null) + throw new ArgumentNullException(nameof(httpClient)); + this.api = api; + _httpClient = httpClient; + _cancellationToken = cancellationToken; + } + public Action RequestStateChanged + { + get; + set; + } + + public async Task MakeRequestAsync(string url, string baseUrl = null, Dictionary payload = null, string method = null) + { + await default(SynchronizationContextRemover); + await api.RateLimit.WaitToProceedAsync(); + if (url[0] != '/') + { + url = "/" + url; + } + string uri2 = (baseUrl ?? api.BaseUrl) + url; + if (method == null) + { + method = api.RequestMethod; + } + Uri uri = api.ProcessRequestUrl(new UriBuilder(uri2), payload, method); + InternalHttpWebRequest request = new InternalHttpWebRequest(uri) + { + Method = method + }; + request.AddHeader("content-type", api.RequestContentType); + request.AddHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36"); + int num3 = request.Timeout = (request.ReadWriteTimeout = (int)api.RequestTimeout.TotalMilliseconds); + await api.ProcessRequestAsync(request, payload); + try + { + RequestStateChanged?.Invoke(this, RequestMakerState.Begin, uri.AbsoluteUri); + using var webHttpRequest = request.ToHttpRequestMessage(); + using var webHttpResponse = await _httpClient.SendAsync(webHttpRequest, _cancellationToken); + string text = await webHttpResponse.Content.ReadAsStringAsync(); + if (!webHttpResponse.IsSuccessStatusCode) + { + if (string.IsNullOrWhiteSpace(text)) + { + throw new APIException($"{webHttpResponse.StatusCode.ConvertInvariant(0)} - {webHttpResponse.StatusCode}"); + } + throw new APIException(text); + } + api.ProcessResponse(new InternalHttpWebResponse(webHttpResponse)); + Action? requestStateChanged = RequestStateChanged; + if (requestStateChanged != null) + { + requestStateChanged!(this, RequestMakerState.Finished, text); + return text; + } + return text; + } + catch (Exception arg) + { + RequestStateChanged?.Invoke(this, RequestMakerState.Error, arg); + throw; + } + } + } +} diff --git a/BTCPayServer.Rating/Services/RateProviderFactory.cs b/BTCPayServer.Rating/Services/RateProviderFactory.cs index 1a3de5e00..e9464d81a 100644 --- a/BTCPayServer.Rating/Services/RateProviderFactory.cs +++ b/BTCPayServer.Rating/Services/RateProviderFactory.cs @@ -78,15 +78,19 @@ namespace BTCPayServer.Services.Rates yield return new AvailableRateProvider("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD"); yield return new AvailableRateProvider("bitbank", "Bitbank", "https://public.bitbank.cc/prices"); yield return new AvailableRateProvider("bitpay", "Bitpay", "https://bitpay.com/rates"); + + yield return new AvailableRateProvider("bitfinex", "Bitfinex", "https://api.bitfinex.com/v2/tickers?symbols=tBTCUSD,tLTCUSD,tLTCBTC,tETHUSD,tETHBTC,tETCBTC,tETCUSD,tRRTUSD,tRRTBTC,tZECUSD,tZECBTC,tXMRUSD,tXMRBTC,tDSHUSD,tDSHBTC,tBTCEUR,tBTCJPY,tXRPUSD,tXRPBTC,tIOTUSD,tIOTBTC,tIOTETH,tEOSUSD,tEOSBTC,tEOSETH,tSANUSD,tSANBTC,tSANETH,tOMGUSD,tOMGBTC,tOMGETH,tNEOUSD,tNEOBTC,tNEOETH,tETPUSD,tETPBTC,tETPETH,tQTMUSD,tQTMBTC,tQTMETH,tAVTUSD,tAVTBTC,tAVTETH,tEDOUSD,tEDOBTC,tEDOETH,tBTGUSD,tBTGBTC,tDATUSD,tDATBTC,tDATETH,tQSHUSD,tQSHBTC,tQSHETH,tYYWUSD,tYYWBTC,tYYWETH,tGNTUSD,tGNTBTC,tGNTETH,tSNTUSD,tSNTBTC,tSNTETH,tIOTEUR,tBATUSD,tBATBTC,tBATETH,tMNAUSD,tMNABTC,tMNAETH,tFUNUSD,tFUNBTC,tFUNETH,tZRXUSD,tZRXBTC,tZRXETH,tTNBUSD,tTNBBTC,tTNBETH,tSPKUSD,tSPKBTC,tSPKETH,tTRXUSD,tTRXBTC,tTRXETH,tRCNUSD,tRCNBTC,tRCNETH,tRLCUSD,tRLCBTC,tRLCETH,tAIDUSD,tAIDBTC,tAIDETH,tSNGUSD,tSNGBTC,tSNGETH,tREPUSD,tREPBTC,tREPETH,tELFUSD,tELFBTC,tELFETH,tNECUSD,tNECBTC,tNECETH,tBTCGBP,tETHEUR,tETHJPY,tETHGBP,tNEOEUR,tNEOJPY,tNEOGBP,tEOSEUR,tEOSJPY,tEOSGBP,tIOTJPY,tIOTGBP,tIOSUSD,tIOSBTC,tIOSETH,tAIOUSD,tAIOBTC,tAIOETH,tREQUSD,tREQBTC,tREQETH,tRDNUSD,tRDNBTC,tRDNETH,tLRCUSD,tLRCBTC,tLRCETH,tWAXUSD,tWAXBTC,tWAXETH,tDAIUSD,tDAIBTC,tDAIETH,tAGIUSD,tAGIBTC,tAGIETH,tBFTUSD,tBFTBTC,tBFTETH,tMTNUSD,tMTNBTC,tMTNETH,tODEUSD,tODEBTC,tODEETH,tANTUSD,tANTBTC,tANTETH,tDTHUSD,tDTHBTC,tDTHETH,tMITUSD,tMITBTC,tMITETH,tSTJUSD,tSTJBTC,tSTJETH,tXLMUSD,tXLMEUR,tXLMJPY,tXLMGBP,tXLMBTC,tXLMETH,tXVGUSD,tXVGEUR,tXVGJPY,tXVGGBP,tXVGBTC,tXVGETH,tBCIUSD,tBCIBTC,tMKRUSD,tMKRBTC,tMKRETH,tKNCUSD,tKNCBTC,tKNCETH,tPOAUSD,tPOABTC,tPOAETH,tEVTUSD,tLYMUSD,tLYMBTC,tLYMETH,tUTKUSD,tUTKBTC,tUTKETH,tVEEUSD,tVEEBTC,tVEEETH,tDADUSD,tDADBTC,tDADETH,tORSUSD,tORSBTC,tORSETH,tAUCUSD,tAUCBTC,tAUCETH,tPOYUSD,tPOYBTC,tPOYETH,tFSNUSD,tFSNBTC,tFSNETH,tCBTUSD,tCBTBTC,tCBTETH,tZCNUSD,tZCNBTC,tZCNETH,tSENUSD,tSENBTC,tSENETH,tNCAUSD,tNCABTC,tNCAETH,tCNDUSD,tCNDBTC,tCNDETH,tCTXUSD,tCTXBTC,tCTXETH,tPAIUSD,tPAIBTC,tSEEUSD,tSEEBTC,tSEEETH,tESSUSD,tESSBTC,tESSETH,tATMUSD,tATMBTC,tATMETH,tHOTUSD,tHOTBTC,tHOTETH,tDTAUSD,tDTABTC,tDTAETH,tIQXUSD,tIQXBTC,tIQXEOS,tWPRUSD,tWPRBTC,tWPRETH,tZILUSD,tZILBTC,tZILETH,tBNTUSD,tBNTBTC,tBNTETH,tABSUSD,tABSETH,tXRAUSD,tXRAETH,tMANUSD,tMANETH,tBBNUSD,tBBNETH,tNIOUSD,tNIOETH,tDGXUSD,tDGXETH,tVETUSD,tVETBTC,tVETETH,tUTNUSD,tUTNETH,tTKNUSD,tTKNETH,tGOTUSD,tGOTEUR,tGOTETH,tXTZUSD,tXTZBTC,tCNNUSD,tCNNETH,tBOXUSD,tBOXETH,tTRXEUR,tTRXGBP,tTRXJPY,tMGOUSD,tMGOETH,tRTEUSD,tRTEETH,tYGGUSD,tYGGETH,tMLNUSD,tMLNETH,tWTCUSD,tWTCETH,tCSXUSD,tCSXETH,tOMNUSD,tOMNBTC,tINTUSD,tINTETH,tDRNUSD,tDRNETH,tPNKUSD,tPNKETH,tDGBUSD,tDGBBTC,tBSVUSD,tBSVBTC,tBABUSD,tBABBTC,tWLOUSD,tWLOXLM,tVLDUSD,tVLDETH,tENJUSD,tENJETH,tONLUSD,tONLETH,tRBTUSD,tRBTBTC,tUSTUSD,tEUTEUR,tEUTUSD,tGSDUSD,tUDCUSD,tTSDUSD,tPAXUSD,tRIFUSD,tRIFBTC,tPASUSD,tPASETH,tVSYUSD,tVSYBTC,tZRXDAI,tMKRDAI,tOMGDAI,tBTTUSD,tBTTBTC,tBTCUST,tETHUST,tCLOUSD,tCLOBTC,tIMPUSD,tIMPETH,tLTCUST,tEOSUST,tBABUST,tSCRUSD,tSCRETH,tGNOUSD,tGNOETH,tGENUSD,tGENETH,tATOUSD,tATOBTC,tATOETH,tWBTUSD,tXCHUSD,tEUSUSD,tWBTETH,tXCHETH,tEUSETH,tLEOUSD,tLEOBTC,tLEOUST,tLEOEOS,tLEOETH,tASTUSD,tASTETH,tFOAUSD,tFOAETH,tUFRUSD,tUFRETH,tZBTUSD,tZBTUST,tOKBUSD,tUSKUSD,tGTXUSD,tKANUSD,tOKBUST,tOKBETH,tOKBBTC,tUSKUST,tUSKETH,tUSKBTC,tUSKEOS,tGTXUST,tKANUST,tAMPUSD,tALGUSD,tALGBTC,tALGUST,tBTCXCH,tSWMUSD,tSWMETH,tTRIUSD,tTRIETH,tLOOUSD,tLOOETH,tAMPUST,tDUSK:USD,tDUSK:BTC,tUOSUSD,tUOSBTC,tRRBUSD,tRRBUST,tDTXUSD,tDTXUST,tAMPBTC,tFTTUSD,tFTTUST,tPAXUST,tUDCUST,tTSDUST,tBTC:CNHT,tUST:CNHT,tCNH:CNHT,tCHZUSD,tCHZUST,tBTCF0:USTF0,tETHF0:USTF0"); + yield return new AvailableRateProvider("okex", "OKEx", "https://www.okex.com/api/futures/v3/instruments/ticker"); + yield return new AvailableRateProvider("coinbasepro", "Coinbase Pro", "https://api.pro.coinbase.com/products"); } void InitExchanges() { // We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request - Providers.Add("binance", new ExchangeSharpRateProvider(new ExchangeBinanceAPI(), true)); - Providers.Add("bittrex", new ExchangeSharpRateProvider(new ExchangeBittrexAPI(), true)); - Providers.Add("poloniex", new ExchangeSharpRateProvider(new ExchangePoloniexAPI(), true)); - Providers.Add("hitbtc", new ExchangeSharpRateProvider(new ExchangeHitBTCAPI(), true)); - Providers.Add("ndax", new ExchangeSharpRateProvider(new ExchangeNDAXAPI(), true)); + AddExchangeSharpProviders("binance"); + AddExchangeSharpProviders("bittrex"); + AddExchangeSharpProviders("poloniex"); + AddExchangeSharpProviders("hitbtc"); + AddExchangeSharpProviders("ndax"); // Handmade providers Providers.Add("coingecko", new CoinGeckoRateProvider(_httpClientFactory)); @@ -99,11 +103,13 @@ namespace BTCPayServer.Services.Rates // Backward compatibility: coinaverage should be using coingecko to prevent stores from breaking Providers.Add("coinaverage", new CoinGeckoRateProvider(_httpClientFactory)); - // Those exchanges make multiple requests when calling GetTickers so we remove them - //DirectProviders.Add("gemini", new ExchangeSharpRateProvider("gemini", new ExchangeGeminiAPI())); - //DirectProviders.Add("bitfinex", new ExchangeSharpRateProvider("bitfinex", new ExchangeBitfinexAPI())); - //DirectProviders.Add("okex", new ExchangeSharpRateProvider("okex", new ExchangeOkexAPI())); - //DirectProviders.Add("bitstamp", new ExchangeSharpRateProvider("bitstamp", new ExchangeBitstampAPI())); + AddExchangeSharpProviders("bitfinex"); + AddExchangeSharpProviders("okex"); + AddExchangeSharpProviders("coinbasepro"); + // Those exchanges make too many requests, exchange sharp do not parallelize so it is too slow... + //AddExchangeSharpProviders("gemini"); + //AddExchangeSharpProviders("bitstamp"); + //AddExchangeSharpProviders("bitmex"); foreach (var provider in Providers.ToArray()) { @@ -112,6 +118,7 @@ namespace BTCPayServer.Services.Rates prov.ValidatyTime = TimeSpan.FromMinutes(5.0); Providers[provider.Key] = prov; } + Providers["gdax"] = Providers["coinbasepro"]; foreach (var supportedExchange in GetCoinGeckoSupportedExchanges()) { @@ -129,6 +136,13 @@ namespace BTCPayServer.Services.Rates } } + private IRateProvider AddExchangeSharpProviders(string providerName) where T: ExchangeAPI, new() + { + var provider = new ExchangeSharpRateProvider(_httpClientFactory.CreateClient($"EXCHANGE_{providerName}".ToUpperInvariant()), true); + Providers.Add(providerName, provider); + return provider; + } + IEnumerable _AvailableRateProviders = null; public IEnumerable GetSupportedExchanges() { @@ -152,8 +166,7 @@ namespace BTCPayServer.Services.Rates { return JArray.Parse(CoinGeckoRateProvider.SupportedExchanges).Select(token => new AvailableRateProvider(Normalize(token["id"].ToString().ToLowerInvariant()), token["id"].ToString().ToLowerInvariant(), token["name"].ToString(), - $"https://api.coingecko.com/api/v3/exchanges/{token["id"]}/tickers", RateSource.Coingecko)) - .Concat(new[] { new AvailableRateProvider("gdax", "gdax", string.Empty, $"https://api.coingecko.com/api/v3/exchanges/gdax", RateSource.Coingecko) }); + $"https://api.coingecko.com/api/v3/exchanges/{token["id"]}/tickers", RateSource.Coingecko)); } private string Normalize(string name) diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index b459e51ea..f5b40d9c8 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -2715,7 +2715,9 @@ noninventoryitem: } // We are not showing a directly implemented exchange as directly implemented in the UI // we need to modify the AvailableRateProvider - if (result.ExpectedName != "coinaverage") + + // There are some exception we stopped supporting but don't want to break backward compat + if (result.ExpectedName != "coinaverage" && result.ExpectedName != "gdax") Assert.Contains(result.ExpectedName, directlySupported); } // Kraken emit one request only after first GetRates From 1ceb5cb57669a9ccf3ed0792de481fae9da086cf Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Sun, 19 Jan 2020 13:57:50 +0100 Subject: [PATCH 131/810] Fix sqlite madness (#1293) * Need to reference Microsoft.EntityFrameworkCore.Design else you can't generate migrations * fix design time migrations issues * update snapshot ``` Your startup project 'BTCPayServer.Data' doesn't reference Microsoft.EntityFrameworkCore.Design. This package is required for the Entity Framework Core Tools to work. Ensure your startup project is c orrect, install the package, and try again. ``` * --- BTCPayServer.Data/BTCPayServer.Data.csproj | 1 + .../Data/ApplicationDbContext.cs | 36 +- .../ApplicationDbContextModelSnapshot.cs | 531 ++++++++++++------ 3 files changed, 366 insertions(+), 202 deletions(-) diff --git a/BTCPayServer.Data/BTCPayServer.Data.csproj b/BTCPayServer.Data/BTCPayServer.Data.csproj index 0a1a49cd6..6639da853 100644 --- a/BTCPayServer.Data/BTCPayServer.Data.csproj +++ b/BTCPayServer.Data/BTCPayServer.Data.csproj @@ -3,6 +3,7 @@ + diff --git a/BTCPayServer.Data/Data/ApplicationDbContext.cs b/BTCPayServer.Data/Data/ApplicationDbContext.cs index 9d0247b0c..a2a24b223 100644 --- a/BTCPayServer.Data/Data/ApplicationDbContext.cs +++ b/BTCPayServer.Data/Data/ApplicationDbContext.cs @@ -2,33 +2,33 @@ using System.Linq; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; using Microsoft.EntityFrameworkCore.Infrastructure; using OpenIddict.EntityFrameworkCore.Models; namespace BTCPayServer.Data { + public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory + { + public ApplicationDbContext CreateDbContext(string[] args) + { + + var builder = new DbContextOptionsBuilder(); + + builder.UseSqlite("Data Source=temp.db"); + + return new ApplicationDbContext(builder.Options, true); + } + } + public class ApplicationDbContext : IdentityDbContext { - //public ApplicationDbContext(): base(CreateMySql()) - //{ + private readonly bool _designTime; - //} - - //private static DbContextOptions CreateMySql() - //{ - // return new DbContextOptionsBuilder() - // .UseMySql("Server=myServerAddress;Database=myDataBase;Uid=myUsername;Pwd=myPassword;") - // .Options; - //} - - public ApplicationDbContext() - { - - } - - public ApplicationDbContext(DbContextOptions options) + public ApplicationDbContext(DbContextOptions options, bool designTime = false) : base(options) { + _designTime = designTime; } public DbSet Invoices @@ -257,7 +257,7 @@ namespace BTCPayServer.Data builder.UseOpenIddict, BTCPayOpenIdToken, string>(); - if (Database.ProviderName == "Microsoft.EntityFrameworkCore.Sqlite") + if (Database.IsSqlite() && !_designTime) { // SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations // here: https://docs.microsoft.com/en-us/ef/core/providers/sqlite/limitations#query-limitations diff --git a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs index e8ee803b4..64a958e63 100644 --- a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -14,31 +14,16 @@ namespace BTCPayServer.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "2.1.14-servicing-32113"); - - modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => - { - b.Property("Address") - .ValueGeneratedOnAdd(); - - b.Property("CreatedTime"); - - b.Property("InvoiceDataId"); - - b.HasKey("Address"); - - b.HasIndex("InvoiceDataId"); - - b.ToTable("AddressInvoices"); - }); + .HasAnnotation("ProductVersion", "3.1.1"); modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => { b.Property("Id") - .ValueGeneratedOnAdd() + .HasColumnType("TEXT") .HasMaxLength(50); b.Property("StoreId") + .HasColumnType("TEXT") .HasMaxLength(50); b.HasKey("Id"); @@ -48,22 +33,46 @@ namespace BTCPayServer.Migrations b.ToTable("ApiKeys"); }); + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("CreatedTime") + .HasColumnType("TEXT"); + + b.Property("InvoiceDataId") + .HasColumnType("TEXT"); + + b.HasKey("Address"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("AddressInvoices"); + }); + modelBuilder.Entity("BTCPayServer.Data.AppData", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .HasColumnType("TEXT"); - b.Property("AppType"); + b.Property("AppType") + .HasColumnType("TEXT"); - b.Property("Created"); + b.Property("Created") + .HasColumnType("TEXT"); - b.Property("Name"); + b.Property("Name") + .HasColumnType("TEXT"); - b.Property("Settings"); + b.Property("Settings") + .HasColumnType("TEXT"); - b.Property("StoreDataId"); + b.Property("StoreDataId") + .HasColumnType("TEXT"); - b.Property("TagAllInvoices"); + b.Property("TagAllInvoices") + .HasColumnType("INTEGER"); b.HasKey("Id"); @@ -75,41 +84,56 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("BTCPayServer.Data.ApplicationUser", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .HasColumnType("TEXT"); - b.Property("AccessFailedCount"); + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); b.Property("ConcurrencyStamp") - .IsConcurrencyToken(); + .IsConcurrencyToken() + .HasColumnType("TEXT"); b.Property("Email") + .HasColumnType("TEXT") .HasMaxLength(256); - b.Property("EmailConfirmed"); + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); - b.Property("LockoutEnabled"); + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); - b.Property("LockoutEnd"); + b.Property("LockoutEnd") + .HasColumnType("TEXT"); b.Property("NormalizedEmail") + .HasColumnType("TEXT") .HasMaxLength(256); b.Property("NormalizedUserName") + .HasColumnType("TEXT") .HasMaxLength(256); - b.Property("PasswordHash"); + b.Property("PasswordHash") + .HasColumnType("TEXT"); - b.Property("PhoneNumber"); + b.Property("PhoneNumber") + .HasColumnType("TEXT"); - b.Property("PhoneNumberConfirmed"); + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); - b.Property("RequiresEmailConfirmation"); + b.Property("RequiresEmailConfirmation") + .HasColumnType("INTEGER"); - b.Property("SecurityStamp"); + b.Property("SecurityStamp") + .HasColumnType("TEXT"); - b.Property("TwoFactorEnabled"); + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); b.Property("UserName") + .HasColumnType("TEXT") .HasMaxLength(256); b.HasKey("Id"); @@ -127,27 +151,35 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdAuthorization", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); - b.Property("ApplicationId"); + b.Property("ApplicationId") + .HasColumnType("TEXT"); b.Property("ConcurrencyToken") .IsConcurrencyToken() + .HasColumnType("TEXT") .HasMaxLength(50); - b.Property("Properties"); + b.Property("Properties") + .HasColumnType("TEXT"); - b.Property("Scopes"); + b.Property("Scopes") + .HasColumnType("TEXT"); b.Property("Status") .IsRequired() + .HasColumnType("TEXT") .HasMaxLength(25); b.Property("Subject") + .HasColumnType("TEXT") .HasMaxLength(450); b.Property("Type") .IsRequired() + .HasColumnType("TEXT") .HasMaxLength(25); b.HasKey("Id"); @@ -160,36 +192,49 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdClient", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); - b.Property("ApplicationUserId"); + b.Property("ApplicationUserId") + .HasColumnType("TEXT"); b.Property("ClientId") .IsRequired() + .HasColumnType("TEXT") .HasMaxLength(100); - b.Property("ClientSecret"); + b.Property("ClientSecret") + .HasColumnType("TEXT"); b.Property("ConcurrencyToken") .IsConcurrencyToken() + .HasColumnType("TEXT") .HasMaxLength(50); - b.Property("ConsentType"); + b.Property("ConsentType") + .HasColumnType("TEXT"); - b.Property("DisplayName"); + b.Property("DisplayName") + .HasColumnType("TEXT"); - b.Property("Permissions"); + b.Property("Permissions") + .HasColumnType("TEXT"); - b.Property("PostLogoutRedirectUris"); + b.Property("PostLogoutRedirectUris") + .HasColumnType("TEXT"); - b.Property("Properties"); + b.Property("Properties") + .HasColumnType("TEXT"); - b.Property("RedirectUris"); + b.Property("RedirectUris") + .HasColumnType("TEXT"); - b.Property("Requirements"); + b.Property("Requirements") + .HasColumnType("TEXT"); b.Property("Type") .IsRequired() + .HasColumnType("TEXT") .HasMaxLength(25); b.HasKey("Id"); @@ -205,36 +250,48 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdToken", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); - b.Property("ApplicationId"); + b.Property("ApplicationId") + .HasColumnType("TEXT"); - b.Property("AuthorizationId"); + b.Property("AuthorizationId") + .HasColumnType("TEXT"); b.Property("ConcurrencyToken") .IsConcurrencyToken() + .HasColumnType("TEXT") .HasMaxLength(50); - b.Property("CreationDate"); + b.Property("CreationDate") + .HasColumnType("TEXT"); - b.Property("ExpirationDate"); + b.Property("ExpirationDate") + .HasColumnType("TEXT"); - b.Property("Payload"); + b.Property("Payload") + .HasColumnType("TEXT"); - b.Property("Properties"); + b.Property("Properties") + .HasColumnType("TEXT"); b.Property("ReferenceId") + .HasColumnType("TEXT") .HasMaxLength(100); b.Property("Status") .IsRequired() + .HasColumnType("TEXT") .HasMaxLength(25); b.Property("Subject") + .HasColumnType("TEXT") .HasMaxLength(450); b.Property("Type") .IsRequired() + .HasColumnType("TEXT") .HasMaxLength(25); b.HasKey("Id"); @@ -251,15 +308,20 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => { - b.Property("InvoiceDataId"); + b.Property("InvoiceDataId") + .HasColumnType("TEXT"); - b.Property("Address"); + b.Property("Address") + .HasColumnType("TEXT"); - b.Property("Assigned"); + b.Property("Assigned") + .HasColumnType("TEXT"); - b.Property("CryptoCode"); + b.Property("CryptoCode") + .HasColumnType("TEXT"); - b.Property("UnAssigned"); + b.Property("UnAssigned") + .HasColumnType("TEXT"); b.HasKey("InvoiceDataId", "Address"); @@ -269,23 +331,31 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .HasColumnType("TEXT"); - b.Property("Blob"); + b.Property("Blob") + .HasColumnType("BLOB"); - b.Property("Created"); + b.Property("Created") + .HasColumnType("TEXT"); - b.Property("CustomerEmail"); + b.Property("CustomerEmail") + .HasColumnType("TEXT"); - b.Property("ExceptionStatus"); + b.Property("ExceptionStatus") + .HasColumnType("TEXT"); - b.Property("ItemCode"); + b.Property("ItemCode") + .HasColumnType("TEXT"); - b.Property("OrderId"); + b.Property("OrderId") + .HasColumnType("TEXT"); - b.Property("Status"); + b.Property("Status") + .HasColumnType("TEXT"); - b.Property("StoreDataId"); + b.Property("StoreDataId") + .HasColumnType("TEXT"); b.HasKey("Id"); @@ -296,13 +366,17 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b => { - b.Property("InvoiceDataId"); + b.Property("InvoiceDataId") + .HasColumnType("TEXT"); - b.Property("UniqueId"); + b.Property("UniqueId") + .HasColumnType("TEXT"); - b.Property("Message"); + b.Property("Message") + .HasColumnType("TEXT"); - b.Property("Timestamp"); + b.Property("Timestamp") + .HasColumnType("TEXT"); b.HasKey("InvoiceDataId", "UniqueId"); @@ -312,15 +386,19 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .HasColumnType("TEXT"); - b.Property("Label"); + b.Property("Label") + .HasColumnType("TEXT"); - b.Property("PairingTime"); + b.Property("PairingTime") + .HasColumnType("TEXT"); - b.Property("SIN"); + b.Property("SIN") + .HasColumnType("TEXT"); - b.Property("StoreDataId"); + b.Property("StoreDataId") + .HasColumnType("TEXT"); b.HasKey("Id"); @@ -334,21 +412,28 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .HasColumnType("TEXT"); - b.Property("DateCreated"); + b.Property("DateCreated") + .HasColumnType("TEXT"); - b.Property("Expiration"); + b.Property("Expiration") + .HasColumnType("TEXT"); - b.Property("Facade"); + b.Property("Facade") + .HasColumnType("TEXT"); - b.Property("Label"); + b.Property("Label") + .HasColumnType("TEXT"); - b.Property("SIN"); + b.Property("SIN") + .HasColumnType("TEXT"); - b.Property("StoreDataId"); + b.Property("StoreDataId") + .HasColumnType("TEXT"); - b.Property("TokenValue"); + b.Property("TokenValue") + .HasColumnType("TEXT"); b.HasKey("Id"); @@ -358,13 +443,16 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .HasColumnType("TEXT"); - b.Property("Accounted"); + b.Property("Accounted") + .HasColumnType("INTEGER"); - b.Property("Blob"); + b.Property("Blob") + .HasColumnType("BLOB"); - b.Property("InvoiceDataId"); + b.Property("InvoiceDataId") + .HasColumnType("TEXT"); b.HasKey("Id"); @@ -376,17 +464,21 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("BTCPayServer.Data.PaymentRequestData", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .HasColumnType("TEXT"); - b.Property("Blob"); + b.Property("Blob") + .HasColumnType("BLOB"); b.Property("Created") .ValueGeneratedOnAdd() + .HasColumnType("TEXT") .HasDefaultValue(new DateTimeOffset(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0))); - b.Property("Status"); + b.Property("Status") + .HasColumnType("INTEGER"); - b.Property("StoreDataId"); + b.Property("StoreDataId") + .HasColumnType("TEXT"); b.HasKey("Id"); @@ -399,7 +491,8 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => { - b.Property("Id"); + b.Property("Id") + .HasColumnType("TEXT"); b.HasKey("Id"); @@ -409,11 +502,13 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .HasColumnType("TEXT"); - b.Property("Blob"); + b.Property("Blob") + .HasColumnType("BLOB"); - b.Property("InvoiceDataId"); + b.Property("InvoiceDataId") + .HasColumnType("TEXT"); b.HasKey("Id"); @@ -425,9 +520,10 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("BTCPayServer.Data.SettingData", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .HasColumnType("TEXT"); - b.Property("Value"); + b.Property("Value") + .HasColumnType("TEXT"); b.HasKey("Id"); @@ -437,23 +533,31 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("BTCPayServer.Data.StoreData", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .HasColumnType("TEXT"); - b.Property("DefaultCrypto"); + b.Property("DefaultCrypto") + .HasColumnType("TEXT"); - b.Property("DerivationStrategies"); + b.Property("DerivationStrategies") + .HasColumnType("TEXT"); - b.Property("DerivationStrategy"); + b.Property("DerivationStrategy") + .HasColumnType("TEXT"); - b.Property("SpeedPolicy"); + b.Property("SpeedPolicy") + .HasColumnType("INTEGER"); - b.Property("StoreBlob"); + b.Property("StoreBlob") + .HasColumnType("BLOB"); - b.Property("StoreCertificate"); + b.Property("StoreCertificate") + .HasColumnType("BLOB"); - b.Property("StoreName"); + b.Property("StoreName") + .HasColumnType("TEXT"); - b.Property("StoreWebsite"); + b.Property("StoreWebsite") + .HasColumnType("TEXT"); b.HasKey("Id"); @@ -463,15 +567,20 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("BTCPayServer.Data.StoredFile", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); - b.Property("ApplicationUserId"); + b.Property("ApplicationUserId") + .HasColumnType("TEXT"); - b.Property("FileName"); + b.Property("FileName") + .HasColumnType("TEXT"); - b.Property("StorageFileName"); + b.Property("StorageFileName") + .HasColumnType("TEXT"); - b.Property("Timestamp"); + b.Property("Timestamp") + .HasColumnType("TEXT"); b.HasKey("Id"); @@ -483,22 +592,28 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .HasColumnType("TEXT"); - b.Property("ApplicationUserId"); + b.Property("ApplicationUserId") + .HasColumnType("TEXT"); b.Property("AttestationCert") - .IsRequired(); + .IsRequired() + .HasColumnType("BLOB"); - b.Property("Counter"); + b.Property("Counter") + .HasColumnType("INTEGER"); b.Property("KeyHandle") - .IsRequired(); + .IsRequired() + .HasColumnType("BLOB"); - b.Property("Name"); + b.Property("Name") + .HasColumnType("TEXT"); b.Property("PublicKey") - .IsRequired(); + .IsRequired() + .HasColumnType("BLOB"); b.HasKey("Id"); @@ -509,11 +624,14 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("BTCPayServer.Data.UserStore", b => { - b.Property("ApplicationUserId"); + b.Property("ApplicationUserId") + .HasColumnType("TEXT"); - b.Property("StoreDataId"); + b.Property("StoreDataId") + .HasColumnType("TEXT"); - b.Property("Role"); + b.Property("Role") + .HasColumnType("TEXT"); b.HasKey("ApplicationUserId", "StoreDataId"); @@ -525,9 +643,10 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("BTCPayServer.Data.WalletData", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .HasColumnType("TEXT"); - b.Property("Blob"); + b.Property("Blob") + .HasColumnType("BLOB"); b.HasKey("Id"); @@ -536,13 +655,17 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b => { - b.Property("WalletDataId"); + b.Property("WalletDataId") + .HasColumnType("TEXT"); - b.Property("TransactionId"); + b.Property("TransactionId") + .HasColumnType("TEXT"); - b.Property("Blob"); + b.Property("Blob") + .HasColumnType("BLOB"); - b.Property("Labels"); + b.Property("Labels") + .HasColumnType("TEXT"); b.HasKey("WalletDataId", "TransactionId"); @@ -552,15 +675,18 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .HasColumnType("TEXT"); b.Property("ConcurrencyStamp") - .IsConcurrencyToken(); + .IsConcurrencyToken() + .HasColumnType("TEXT"); b.Property("Name") + .HasColumnType("TEXT") .HasMaxLength(256); b.Property("NormalizedName") + .HasColumnType("TEXT") .HasMaxLength(256); b.HasKey("Id"); @@ -575,14 +701,18 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("ClaimType"); + b.Property("ClaimType") + .HasColumnType("TEXT"); - b.Property("ClaimValue"); + b.Property("ClaimValue") + .HasColumnType("TEXT"); b.Property("RoleId") - .IsRequired(); + .IsRequired() + .HasColumnType("TEXT"); b.HasKey("Id"); @@ -594,14 +724,18 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.Property("ClaimType"); + b.Property("ClaimType") + .HasColumnType("TEXT"); - b.Property("ClaimValue"); + b.Property("ClaimValue") + .HasColumnType("TEXT"); b.Property("UserId") - .IsRequired(); + .IsRequired() + .HasColumnType("TEXT"); b.HasKey("Id"); @@ -612,14 +746,18 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { - b.Property("LoginProvider"); + b.Property("LoginProvider") + .HasColumnType("TEXT"); - b.Property("ProviderKey"); + b.Property("ProviderKey") + .HasColumnType("TEXT"); - b.Property("ProviderDisplayName"); + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); b.Property("UserId") - .IsRequired(); + .IsRequired() + .HasColumnType("TEXT"); b.HasKey("LoginProvider", "ProviderKey"); @@ -630,9 +768,11 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => { - b.Property("UserId"); + b.Property("UserId") + .HasColumnType("TEXT"); - b.Property("RoleId"); + b.Property("RoleId") + .HasColumnType("TEXT"); b.HasKey("UserId", "RoleId"); @@ -643,13 +783,17 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { - b.Property("UserId"); + b.Property("UserId") + .HasColumnType("TEXT"); - b.Property("LoginProvider"); + b.Property("LoginProvider") + .HasColumnType("TEXT"); - b.Property("Name"); + b.Property("Name") + .HasColumnType("TEXT"); - b.Property("Value"); + b.Property("Value") + .HasColumnType("TEXT"); b.HasKey("UserId", "LoginProvider", "Name"); @@ -659,23 +803,30 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictScope", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); b.Property("ConcurrencyToken") .IsConcurrencyToken() + .HasColumnType("TEXT") .HasMaxLength(50); - b.Property("Description"); + b.Property("Description") + .HasColumnType("TEXT"); - b.Property("DisplayName"); + b.Property("DisplayName") + .HasColumnType("TEXT"); b.Property("Name") .IsRequired() + .HasColumnType("TEXT") .HasMaxLength(200); - b.Property("Properties"); + b.Property("Properties") + .HasColumnType("TEXT"); - b.Property("Resources"); + b.Property("Resources") + .HasColumnType("TEXT"); b.HasKey("Id"); @@ -685,14 +836,6 @@ namespace BTCPayServer.Migrations b.ToTable("OpenIddictScopes"); }); - 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") @@ -701,6 +844,14 @@ namespace BTCPayServer.Migrations .OnDelete(DeleteBehavior.Cascade); }); + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("AddressInvoices") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("BTCPayServer.Data.AppData", b => { b.HasOne("BTCPayServer.Data.StoreData", "StoreData") @@ -739,7 +890,8 @@ namespace BTCPayServer.Migrations b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") .WithMany("HistoricalAddressInvoices") .HasForeignKey("InvoiceDataId") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => @@ -755,7 +907,8 @@ namespace BTCPayServer.Migrations b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") .WithMany("Events") .HasForeignKey("InvoiceDataId") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b => @@ -787,7 +940,8 @@ namespace BTCPayServer.Migrations b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") .WithMany("PendingInvoices") .HasForeignKey("Id") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => @@ -817,12 +971,14 @@ namespace BTCPayServer.Migrations b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") .WithMany("UserStores") .HasForeignKey("ApplicationUserId") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.HasOne("BTCPayServer.Data.StoreData", "StoreData") .WithMany("UserStores") .HasForeignKey("StoreDataId") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b => @@ -830,52 +986,59 @@ namespace BTCPayServer.Migrations b.HasOne("BTCPayServer.Data.WalletData", "WalletData") .WithMany("WalletTransactions") .HasForeignKey("WalletDataId") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) .WithMany() .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { - b.HasOne("BTCPayServer.Data.ApplicationUser") + b.HasOne("BTCPayServer.Data.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { - b.HasOne("BTCPayServer.Data.ApplicationUser") + b.HasOne("BTCPayServer.Data.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) .WithMany() .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("BTCPayServer.Data.ApplicationUser") + b.HasOne("BTCPayServer.Data.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { - b.HasOne("BTCPayServer.Data.ApplicationUser") + b.HasOne("BTCPayServer.Data.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); #pragma warning restore 612, 618 } From 7e073fb7e1332a8a7f69d7e5bbb6a4a48d5c04ed Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sun, 19 Jan 2020 22:10:05 +0900 Subject: [PATCH 132/810] Add test CanCreateSqlitedb --- BTCPayServer.Tests/UnitTest1.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index f5b40d9c8..3b6b0fcb3 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -2887,6 +2887,18 @@ noninventoryitem: return name; } + [Fact(Timeout = TestTimeout)] + [Trait("Fast", "Fast")] + public async Task CanCreateSqlitedb() + { + if (File.Exists("temp.db")) + File.Delete("temp.db"); + // This test sqlite can migrate + var builder = new DbContextOptionsBuilder(); + builder.UseSqlite("Data Source=temp.db"); + await new ApplicationDbContext(builder.Options).Database.MigrateAsync(); + } + [Fact(Timeout = TestTimeout)] [Trait("Fast", "Fast")] public void CheckRatesProvider() From 61b99f6630dbd9c1ae6b3742ffe66bb11c93f418 Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Tue, 21 Jan 2020 05:19:55 +0100 Subject: [PATCH 133/810] Add support for ETB liquid asset (#1295) --- .../Liquid/BTCPayNetworkProvider.Liquid.cs | 16 +------- .../BTCPayNetworkProvider.LiquidAssets.cs | 38 +++++++++++------- BTCPayServer/wwwroot/imlegacy/etb.png | Bin 0 -> 31990 bytes 3 files changed, 24 insertions(+), 30 deletions(-) create mode 100644 BTCPayServer/wwwroot/imlegacy/etb.png diff --git a/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.Liquid.cs b/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.Liquid.cs index d87f191af..cacea5c59 100644 --- a/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.Liquid.cs +++ b/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.Liquid.cs @@ -31,21 +31,7 @@ namespace BTCPayServer CryptoImagePath = "imlegacy/liquid.png", DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"), - SupportRBF = true, - //https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py - ElectrumMapping = NetworkType == NetworkType.Mainnet - ? new Dictionary() - { - {0x0488b21eU, DerivationType.Legacy }, // xpub - {0x049d7cb2U, DerivationType.SegwitP2SH }, // ypub - {0x4b24746U, DerivationType.Segwit }, //zpub - } - : new Dictionary() - { - {0x043587cfU, DerivationType.Legacy}, - {0x044a5262U, DerivationType.SegwitP2SH}, - {0x045f1cf6U, DerivationType.Segwit} - }, + SupportRBF = true }); } } diff --git a/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.LiquidAssets.cs b/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.LiquidAssets.cs index 4b9d14ca5..231af2278 100644 --- a/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.LiquidAssets.cs +++ b/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.LiquidAssets.cs @@ -30,21 +30,29 @@ namespace BTCPayServer CryptoImagePath = "imlegacy/liquid-tether.svg", DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"), - SupportRBF = true, - //https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py - ElectrumMapping = NetworkType == NetworkType.Mainnet - ? new Dictionary() - { - {0x0488b21eU, DerivationType.Legacy }, // xpub - {0x049d7cb2U, DerivationType.SegwitP2SH }, // ypub - {0x4b24746U, DerivationType.Segwit }, //zpub - } - : new Dictionary() - { - {0x043587cfU, DerivationType.Legacy}, - {0x044a5262U, DerivationType.SegwitP2SH}, - {0x045f1cf6U, DerivationType.Segwit} - } + SupportRBF = true + }); + + Add(new ElementsBTCPayNetwork() + { + CryptoCode = "ETB", + NetworkCryptoCode = "LBTC", + DefaultRateRules = new[] + { + + "ETB_X = ETB_BTC * BTC_X", + "ETB_BTC = bitpay(ETB_BTC)" + }, + Divisibility = 2, + AssetId = new uint256("ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"), + DisplayName = "Ethiopian Birr", + BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}", + NBXplorerNetwork = nbxplorerNetwork, + UriScheme = "liquidnetwork", + CryptoImagePath = "imlegacy/etb.png", + DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), + CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"), + SupportRBF = true }); } } diff --git a/BTCPayServer/wwwroot/imlegacy/etb.png b/BTCPayServer/wwwroot/imlegacy/etb.png new file mode 100644 index 0000000000000000000000000000000000000000..363cff51c752022817f0ce7f8c9cae5d301ad9fa GIT binary patch literal 31990 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 Date: Tue, 21 Jan 2020 13:20:52 +0900 Subject: [PATCH 134/810] bump --- Build/Version.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Build/Version.csproj b/Build/Version.csproj index 36b9c0027..3142adbaf 100644 --- a/Build/Version.csproj +++ b/Build/Version.csproj @@ -1,5 +1,5 @@ - 1.0.3.153 + 1.0.3.154 From 6848482999910d4480773a24c6bab407e0565023 Mon Sep 17 00:00:00 2001 From: Nicolas Dorier Date: Tue, 21 Jan 2020 16:53:24 +0900 Subject: [PATCH 135/810] Remove the next address to pay to from Invoice details page (Fix #1056) (#1283) --- BTCPayServer/Controllers/InvoiceController.UI.cs | 1 - BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs | 1 - BTCPayServer/Views/Invoice/InvoicePaymentsPartial.cshtml | 6 +----- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 05d85c59d..d5f1b90a7 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -104,7 +104,6 @@ namespace BTCPayServer.Controllers cryptoPayment.Paid = _CurrencyNameTable.DisplayFormatCurrency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode); cryptoPayment.Overpaid = _CurrencyNameTable.DisplayFormatCurrency(accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode); var paymentMethodDetails = data.GetPaymentMethodDetails(); - cryptoPayment.Address = paymentMethodDetails.GetPaymentDestination(); cryptoPayment.Rate = ExchangeRate(data); model.CryptoPayments.Add(cryptoPayment); } diff --git a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs index 359da32ec..9f44b61c4 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs @@ -36,7 +36,6 @@ namespace BTCPayServer.Models.InvoicingModels public string PaymentMethod { get; set; } public string Due { get; set; } public string Paid { get; set; } - public string Address { get; internal set; } public string Rate { get; internal set; } public string PaymentUrl { get; internal set; } public string Overpaid { get; set; } diff --git a/BTCPayServer/Views/Invoice/InvoicePaymentsPartial.cshtml b/BTCPayServer/Views/Invoice/InvoicePaymentsPartial.cshtml index 3d9d0806a..36810a1f3 100644 --- a/BTCPayServer/Views/Invoice/InvoicePaymentsPartial.cshtml +++ b/BTCPayServer/Views/Invoice/InvoicePaymentsPartial.cshtml @@ -3,12 +3,11 @@
-

Paid summary

+

Current status

- @@ -23,9 +22,6 @@ { - From 8e6f43cd3ac56fbb91a3479db767a012e681514f Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Tue, 21 Jan 2020 09:33:12 +0100 Subject: [PATCH 136/810] Sign with NBX Seed (#1218) --- BTCPayServer.Tests/SeleniumTests.cs | 28 ++++++++++++++++ .../Controllers/WalletsController.PSBT.cs | 14 ++++++++ BTCPayServer/Controllers/WalletsController.cs | 33 ++++++++++++++++--- .../WalletViewModels/WalletPSBTViewModel.cs | 2 ++ .../WalletViewModels/WalletSendModel.cs | 2 ++ BTCPayServer/Views/Wallets/WalletPSBT.cshtml | 5 +++ BTCPayServer/Views/Wallets/WalletSend.cshtml | 5 +++ 7 files changed, 85 insertions(+), 4 deletions(-) diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 9dd48bda3..f507f0c76 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -432,6 +432,13 @@ namespace BTCPayServer.Tests //let's test quickly the receive wallet page s.Driver.FindElement(By.Id("Wallets")).Click(); s.Driver.FindElement(By.LinkText("Manage")).Click(); + + s.Driver.FindElement(By.Id("WalletSend")).Click(); + s.Driver.ScrollTo(By.Id("SendMenu")); + s.Driver.FindElement(By.Id("SendMenu")).ForceClick(); + //you cant use the Sign with NBX option without saving private keys when generating the wallet. + Assert.DoesNotContain("nbx-seed", s.Driver.PageSource); + s.Driver.FindElement(By.Id("WalletReceive")).Click(); //generate a receiving address s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click(); @@ -472,16 +479,19 @@ namespace BTCPayServer.Tests var address = invoice.EntityToDTO().Addresses["BTC"]; + //wallet should have been imported to bitcoin core wallet in watch only mode. var result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest)); Assert.True(result.IsWatchOnly); s.GoToStore(storeId.storeId); var mnemonic = s.GenerateWallet("BTC", "", true, true); + //lets import and save private keys var root = new Mnemonic(mnemonic).DeriveExtKey(); invoiceId = s.CreateInvoice(storeId.storeId); invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice( invoiceId); address = invoice.EntityToDTO().Addresses["BTC"]; result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest)); + //spendable from bitcoin core wallet! Assert.False(result.IsWatchOnly); var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create(address, Network.RegTest), Money.Coins(3.0m)); s.Server.ExplorerNode.Generate(1); @@ -538,7 +548,25 @@ namespace BTCPayServer.Tests checkboxElement.Click(); } } + SignWith(mnemonic); + + s.Driver.FindElement(By.Id("Wallets")).Click(); + s.Driver.FindElement(By.LinkText("Manage")).Click(); + s.Driver.FindElement(By.Id("WalletSend")).Click(); + + var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest); + SetTransactionOutput(0, jack, 0.01m); + s.Driver.ScrollTo(By.Id("SendMenu")); + s.Driver.FindElement(By.Id("SendMenu")).ForceClick(); + + s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click(); + Assert.Contains(jack.ToString(), s.Driver.PageSource); + Assert.Contains("0.01000000", s.Driver.PageSource); + s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick(); + Assert.Equal(walletTransactionLink, s.Driver.Url); + + } } } diff --git a/BTCPayServer/Controllers/WalletsController.PSBT.cs b/BTCPayServer/Controllers/WalletsController.PSBT.cs index a15126f14..789494528 100644 --- a/BTCPayServer/Controllers/WalletsController.PSBT.cs +++ b/BTCPayServer/Controllers/WalletsController.PSBT.cs @@ -7,6 +7,7 @@ using BTCPayServer.ModelBinders; using BTCPayServer.Models.WalletViewModels; using Microsoft.AspNetCore.Mvc; using NBitcoin; +using NBXplorer; using NBXplorer.Models; namespace BTCPayServer.Controllers @@ -56,6 +57,9 @@ namespace BTCPayServer.Controllers { var network = NetworkProvider.GetNetwork(walletId.CryptoCode); vm.CryptoCode = network.CryptoCode; + vm.NBXSeedAvailable = await CanUseHotWallet() && !string.IsNullOrEmpty(await ExplorerClientProvider.GetExplorerClient(network) + .GetMetadataAsync(GetDerivationSchemeSettings(walletId).AccountDerivation, + WellknownMetadataKeys.Mnemonic)); if (await vm.GetPSBT(network.NBitcoinNetwork) is PSBT psbt) { vm.Decoded = psbt.ToString(); @@ -106,6 +110,16 @@ namespace BTCPayServer.Controllers return RedirectToWalletPSBT(walletId, psbt, vm.FileName); case "seed": return SignWithSeed(walletId, psbt.ToBase64()); + case "nbx-seed": + var derivationScheme = GetDerivationSchemeSettings(walletId); + var extKey = await ExplorerClientProvider.GetExplorerClient(network) + .GetMetadataAsync(derivationScheme.AccountDerivation, WellknownMetadataKeys.MasterHDKey); + + return await SignWithSeed(walletId, new SignWithSeedViewModel() + { + SeedOrKey = extKey, + PSBT = psbt.ToBase64() + }); case "broadcast": { return await WalletPSBTReady(walletId, psbt.ToBase64()); diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index cfe1a14ff..856b8d8b5 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -29,6 +29,7 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using NBitcoin; using NBitcoin.DataEncoders; +using NBXplorer; using NBXplorer.DerivationStrategy; using NBXplorer.Models; using Newtonsoft.Json; @@ -54,6 +55,7 @@ namespace BTCPayServer.Controllers private readonly BTCPayWalletProvider _walletProvider; private readonly WalletReceiveStateService _WalletReceiveStateService; private readonly EventAggregator _EventAggregator; + private readonly SettingsRepository _settingsRepository; public RateFetcher RateFetcher { get; } CurrencyNameTable _currencyTable; @@ -70,7 +72,8 @@ namespace BTCPayServer.Controllers IFeeProviderFactory feeRateProvider, BTCPayWalletProvider walletProvider, WalletReceiveStateService walletReceiveStateService, - EventAggregator eventAggregator) + EventAggregator eventAggregator, + SettingsRepository settingsRepository) { _currencyTable = currencyTable; Repository = repo; @@ -86,6 +89,7 @@ namespace BTCPayServer.Controllers _walletProvider = walletProvider; _WalletReceiveStateService = walletReceiveStateService; _EventAggregator = eventAggregator; + _settingsRepository = settingsRepository; } // Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md @@ -368,6 +372,15 @@ namespace BTCPayServer.Controllers return RedirectToAction(nameof(WalletReceive), new {walletId}); } + private async Task CanUseHotWallet() + { + var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings.Key)).Succeeded; + if (isAdmin) + return true; + var policies = await _settingsRepository.GetSettingAsync(); + return policies?.AllowHotWalletForAll is true; + } + [HttpGet] [Route("{walletId}/send")] public async Task WalletSend( @@ -405,6 +418,9 @@ namespace BTCPayServer.Controllers var feeProvider = _feeRateProvider.CreateFeeProvider(network); var recommendedFees = feeProvider.GetFeeRateAsync(); var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.AccountDerivation); + model.NBXSeedAvailable = await CanUseHotWallet() && !string.IsNullOrEmpty(await ExplorerClientProvider.GetExplorerClient(network) + .GetMetadataAsync(GetDerivationSchemeSettings(walletId).AccountDerivation, + WellknownMetadataKeys.Mnemonic)); model.CurrentBalance = await balance; model.RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi; model.FeeSatoshiPerByte = model.RecommendedSatoshiPerByte; @@ -554,6 +570,15 @@ namespace BTCPayServer.Controllers { case "vault": return ViewVault(walletId, psbt.PSBT); + case "nbx-seed": + var extKey = await ExplorerClientProvider.GetExplorerClient(network) + .GetMetadataAsync(derivationScheme.AccountDerivation, WellknownMetadataKeys.MasterHDKey, cancellation); + + return await SignWithSeed(walletId, new SignWithSeedViewModel() + { + SeedOrKey = extKey, + PSBT = psbt.PSBT.ToBase64() + }); case "ledger": return ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress); case "seed": @@ -643,7 +668,7 @@ namespace BTCPayServer.Controllers { if (!ModelState.IsValid) { - return View(viewModel); + return View("SignWithSeed", viewModel); } var network = NetworkProvider.GetNetwork(walletId.CryptoCode); if (network == null) @@ -666,7 +691,7 @@ namespace BTCPayServer.Controllers if (!ModelState.IsValid) { - return View(viewModel); + return View("SignWithSeed", viewModel); } ExtKey signingKey = null; @@ -679,7 +704,7 @@ namespace BTCPayServer.Controllers if (rootedKeyPath == null) { ModelState.AddModelError(nameof(viewModel.SeedOrKey), "The master fingerprint and/or account key path of your seed are not set in the wallet settings."); - return View(viewModel); + return View("SignWithSeed", viewModel); } // The user gave the root key, let's try to rebase the PSBT, and derive the account private key if (rootedKeyPath.MasterFingerprint == extKey.GetPublicKey().GetHDFingerPrint()) diff --git a/BTCPayServer/Models/WalletViewModels/WalletPSBTViewModel.cs b/BTCPayServer/Models/WalletViewModels/WalletPSBTViewModel.cs index 1619ea109..fc627d624 100644 --- a/BTCPayServer/Models/WalletViewModels/WalletPSBTViewModel.cs +++ b/BTCPayServer/Models/WalletViewModels/WalletPSBTViewModel.cs @@ -13,6 +13,8 @@ namespace BTCPayServer.Models.WalletViewModels public string CryptoCode { get; set; } public string Decoded { get; set; } string _FileName; + public bool NBXSeedAvailable { get; set; } + public string FileName { get diff --git a/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs b/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs index c7a52b937..73c72e121 100644 --- a/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs +++ b/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs @@ -45,5 +45,7 @@ namespace BTCPayServer.Models.WalletViewModels public bool SupportRBF { get; set; } [Display(Name = "Disable RBF")] public bool DisableRBF { get; set; } + + public bool NBXSeedAvailable { get; set; } } } diff --git a/BTCPayServer/Views/Wallets/WalletPSBT.cshtml b/BTCPayServer/Views/Wallets/WalletPSBT.cshtml index f1046ff27..e090eb6af 100644 --- a/BTCPayServer/Views/Wallets/WalletPSBT.cshtml +++ b/BTCPayServer/Views/Wallets/WalletPSBT.cshtml @@ -30,6 +30,7 @@
+
From 1bfe9dda97762954a2e86350707acdf8732c2fd1 Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Tue, 21 Jan 2020 10:27:10 +0100 Subject: [PATCH 137/810] Integrate Configurator External Service (#1190) --- .gitignore | 1 + .../Configuration/BTCPayServerOptions.cs | 1 + .../Configuration/ExternalConnectionString.cs | 3 +- BTCPayServer/Configuration/ExternalService.cs | 21 +++++++++--- BTCPayServer/Controllers/ServerController.cs | 15 +++++++-- BTCPayServer/Properties/launchSettings.json | 1 + .../Views/Server/ConfiguratorService.cshtml | 33 +++++++++++++++++++ BTCPayServer/Views/Server/Services.cshtml | 13 ++++++-- 8 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 BTCPayServer/Views/Server/ConfiguratorService.cshtml diff --git a/.gitignore b/.gitignore index 35908eca9..fbf55eb42 100644 --- a/.gitignore +++ b/.gitignore @@ -293,3 +293,4 @@ BTCPayServer/wwwroot/bundles/* !BTCPayServer/wwwroot/bundles/.gitignore .vscode +BTCPayServer/testpwd diff --git a/BTCPayServer/Configuration/BTCPayServerOptions.cs b/BTCPayServer/Configuration/BTCPayServerOptions.cs index de189636e..37c4a9d27 100644 --- a/BTCPayServer/Configuration/BTCPayServerOptions.cs +++ b/BTCPayServer/Configuration/BTCPayServerOptions.cs @@ -141,6 +141,7 @@ namespace BTCPayServer.Configuration ExternalServices.Load(net.CryptoCode, conf); } + ExternalServices.LoadNonCryptoServices(conf); Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray())); var services = conf.GetOrDefault("externalservices", null); diff --git a/BTCPayServer/Configuration/ExternalConnectionString.cs b/BTCPayServer/Configuration/ExternalConnectionString.cs index 24c90411d..2c0114138 100644 --- a/BTCPayServer/Configuration/ExternalConnectionString.cs +++ b/BTCPayServer/Configuration/ExternalConnectionString.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Controllers; @@ -77,7 +78,7 @@ namespace BTCPayServer.Configuration } } - if (serviceType == ExternalServiceTypes.Charge || serviceType == ExternalServiceTypes.RTL || serviceType == ExternalServiceTypes.Spark) + if (new []{ExternalServiceTypes.Charge, ExternalServiceTypes.RTL, ExternalServiceTypes.Spark, ExternalServiceTypes.Configurator}.Contains(serviceType)) { // Read access key from cookie file if (connectionString.CookieFilePath != null) diff --git a/BTCPayServer/Configuration/ExternalService.cs b/BTCPayServer/Configuration/ExternalService.cs index 1fc05215b..ccee3cb34 100644 --- a/BTCPayServer/Configuration/ExternalService.cs +++ b/BTCPayServer/Configuration/ExternalService.cs @@ -41,11 +41,20 @@ namespace BTCPayServer.Configuration $"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine + "Error: {1}", "C-Lightning (Charge server)"); + + } + + public void LoadNonCryptoServices(IConfiguration configuration) + { + Load(configuration, null, "configurator", ExternalServiceTypes.Configurator, "Invalid setting {0}, " + Environment.NewLine + + $"configurator: 'passwordfile=/etc/configurator/password'" + Environment.NewLine + + "Error: {1}", + "Configurator"); } void Load(IConfiguration configuration, string cryptoCode, string serviceName, ExternalServiceTypes type, string errorMessage, string displayName) { - var setting = $"{cryptoCode}.external.{serviceName}"; + var setting = $"{(!string.IsNullOrEmpty(cryptoCode)? $"{cryptoCode}.": string.Empty)}external.{serviceName}"; var connStr = configuration.GetOrDefault(setting, string.Empty); if (connStr.Length != 0) { @@ -65,8 +74,11 @@ namespace BTCPayServer.Configuration public ExternalService GetService(string serviceName, string cryptoCode) { - return this.FirstOrDefault(o => o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) && - o.ServiceName.Equals(serviceName, StringComparison.OrdinalIgnoreCase)); + return this.FirstOrDefault(o => + (cryptoCode == null && o.CryptoCode == null) || + (o.CryptoCode != null && o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase)) + && + o.ServiceName.Equals(serviceName, StringComparison.OrdinalIgnoreCase)); } } @@ -88,6 +100,7 @@ namespace BTCPayServer.Configuration RTL, Charge, P2P, - RPC + RPC, + Configurator } } diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index 7455f0d80..ec2d19331 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -528,10 +528,12 @@ namespace BTCPayServer.Controllers return null; } - [Route("server/services/{serviceName}/{cryptoCode}")] + + + [Route("server/services/{serviceName}/{cryptoCode?}")] public async Task Service(string serviceName, string cryptoCode, bool showQR = false, uint? nonce = null) { - if (!_dashBoard.IsFullySynched(cryptoCode, out _)) + if (!string.IsNullOrEmpty(cryptoCode) && !_dashBoard.IsFullySynched(cryptoCode, out _)) { TempData[WellKnownTempData.ErrorMessage] = $"{cryptoCode} is not fully synched"; return RedirectToAction(nameof(Services)); @@ -542,6 +544,7 @@ namespace BTCPayServer.Controllers try { + if (service.Type == ExternalServiceTypes.P2P) { return View("P2PService", new LightningWalletServices() @@ -595,6 +598,14 @@ namespace BTCPayServer.Controllers case ExternalServiceTypes.LNDGRPC: case ExternalServiceTypes.LNDRest: return LndServices(service, connectionString, nonce); + case ExternalServiceTypes.Configurator: + return View("ConfiguratorService", + new LightningWalletServices() + { + ShowQR = showQR, + WalletName = service.ServiceName, + ServiceLink = $"{connectionString.Server}?password={connectionString.AccessKey}" + }); default: throw new NotSupportedException(service.Type.ToString()); } diff --git a/BTCPayServer/Properties/launchSettings.json b/BTCPayServer/Properties/launchSettings.json index b578d2481..2f3db0eaa 100644 --- a/BTCPayServer/Properties/launchSettings.json +++ b/BTCPayServer/Properties/launchSettings.json @@ -40,6 +40,7 @@ "BTCPAY_BTCEXTERNALLNDSEEDBACKUP": "../BTCPayServer.Tests/TestData/LndSeedBackup/walletunlock.json", "BTCPAY_BTCEXTERNALSPARK": "server=/spark/btc/;cookiefile=fake", "BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/mycharge/btc/;cookiefilepath=fake", + "BTCPAY_EXTERNALCONFIGURATOR": "passwordfile=testpwd;server=/configurator", "BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/", "BTCPAY_ALLOW-ADMIN-REGISTRATION": "true", "BTCPAY_DISABLE-REGISTRATION": "false", diff --git a/BTCPayServer/Views/Server/ConfiguratorService.cshtml b/BTCPayServer/Views/Server/ConfiguratorService.cshtml new file mode 100644 index 000000000..5106650d1 --- /dev/null +++ b/BTCPayServer/Views/Server/ConfiguratorService.cshtml @@ -0,0 +1,33 @@ +@model LightningWalletServices +@{ + ViewData.SetActivePageAndTitle(ServerNavPages.Services); +} + +

BTCPay Server Configurator

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

+ This page exposes information to use the configured BTCPay Server Configurator to modify this setup. +

+
+ + + + + +
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") + +} diff --git a/BTCPayServer/Views/Server/Services.cshtml b/BTCPayServer/Views/Server/Services.cshtml index 4794610c2..33e18d34e 100644 --- a/BTCPayServer/Views/Server/Services.cshtml +++ b/BTCPayServer/Views/Server/Services.cshtml @@ -29,7 +29,7 @@
- @foreach (var s in Model.ExternalServices) + @foreach (var s in Model.ExternalServices.Where(service => !string.IsNullOrEmpty(service.CryptoCode))) { @@ -52,7 +52,7 @@ -@if (Model.OtherExternalServices.Count != 0) +@if (Model.OtherExternalServices.Count != 0 || Model.ExternalServices.Any(service => string.IsNullOrEmpty(service.CryptoCode))) {
@@ -69,6 +69,15 @@
+ @foreach (var s in Model.ExternalServices.Where(service => string.IsNullOrEmpty(service.CryptoCode))) + { + + + + + } @foreach (var s in Model.OtherExternalServices) { From 78d8f4e011270a7ff08d28c160990800d62d384b Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 21 Jan 2020 20:54:45 +0900 Subject: [PATCH 138/810] Fix rescan wallet link --- BTCPayServer/Views/Wallets/WalletTransactions.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/Views/Wallets/WalletTransactions.cshtml b/BTCPayServer/Views/Wallets/WalletTransactions.cshtml index 260215097..bc4cfe928 100644 --- a/BTCPayServer/Views/Wallets/WalletTransactions.cshtml +++ b/BTCPayServer/Views/Wallets/WalletTransactions.cshtml @@ -57,7 +57,7 @@ }
- If BTCPay Server shows you an invalid balance, rescan your wallet.
+ If BTCPay Server shows you an invalid balance, rescan your wallet.
If some transactions appear in BTCPay Server, but are missing on Electrum or another wallet, follow those instructions.
From bdb2edba12517ca49a1a7aa299ca408ca47d218f Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 21 Jan 2020 21:00:34 +0900 Subject: [PATCH 139/810] Fix U2F signing --- BTCPayServer/Controllers/WalletsController.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index 856b8d8b5..69205c390 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -580,7 +580,7 @@ namespace BTCPayServer.Controllers PSBT = psbt.PSBT.ToBase64() }); case "ledger": - return ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress); + return ViewWalletSendLedger(walletId, psbt.PSBT, psbt.ChangeAddress); case "seed": return SignWithSeed(walletId, psbt.PSBT.ToBase64()); case "analyze-psbt": @@ -641,14 +641,14 @@ namespace BTCPayServer.Controllers return null; } - private ViewResult ViewWalletSendLedger(PSBT psbt, BitcoinAddress hintChange = null) + private ViewResult ViewWalletSendLedger(WalletId walletId, PSBT psbt, BitcoinAddress hintChange = null) { SetAmbientPSBT(psbt); return View("WalletSendLedger", new WalletSendLedgerModel() { PSBT = psbt.ToBase64(), HintChange = hintChange?.ToString(), - WebsocketPath = this.Url.Action(nameof(LedgerConnection)) + WebsocketPath = this.Url.Action(nameof(LedgerConnection), new { walletId = walletId.ToString() }) }); } From d0bfa674959dd2f6e7bbc66b6132ac474cda44d5 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 21 Jan 2020 21:04:35 +0900 Subject: [PATCH 140/810] Fix build --- BTCPayServer/Controllers/WalletsController.PSBT.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/Controllers/WalletsController.PSBT.cs b/BTCPayServer/Controllers/WalletsController.PSBT.cs index 789494528..2c79f1d73 100644 --- a/BTCPayServer/Controllers/WalletsController.PSBT.cs +++ b/BTCPayServer/Controllers/WalletsController.PSBT.cs @@ -97,7 +97,7 @@ namespace BTCPayServer.Controllers case "vault": return ViewVault(walletId, psbt); case "ledger": - return ViewWalletSendLedger(psbt); + return ViewWalletSendLedger(walletId, psbt); case "update": var derivationSchemeSettings = GetDerivationSchemeSettings(walletId); psbt = await UpdatePSBT(derivationSchemeSettings, psbt, network); From 80ee03d89760b0721bd633f8f067dd8e0022a859 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 21 Jan 2020 21:06:35 +0900 Subject: [PATCH 141/810] Remove dead link --- BTCPayServer/Views/Stores/Rates.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/Views/Stores/Rates.cshtml b/BTCPayServer/Views/Stores/Rates.cshtml index 77a6ecc4c..663444b76 100644 --- a/BTCPayServer/Views/Stores/Rates.cshtml +++ b/BTCPayServer/Views/Stores/Rates.cshtml @@ -19,7 +19,7 @@
Scripting

Rate script allows you to express precisely how you want to calculate rates for currency pairs.

-

We are retrieving the rate of each exchange either directly, via CoinGecko (free) or BitcoinAverage (commercial)

+

We are retrieving the rate of each exchange either directly, via CoinGecko (free).

From c68bf5220e800f5cc366c3c6bc5f2343c61aa0fc Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 21 Jan 2020 21:09:49 +0900 Subject: [PATCH 142/810] bump --- Build/Version.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Build/Version.csproj b/Build/Version.csproj index 3142adbaf..d554d873e 100644 --- a/Build/Version.csproj +++ b/Build/Version.csproj @@ -1,5 +1,5 @@ - 1.0.3.154 + 1.0.3.155 From 71671b9e164e5a181781774e22bdee68d1ed4fc5 Mon Sep 17 00:00:00 2001 From: Kukks Date: Wed, 18 Sep 2019 18:26:24 +0300 Subject: [PATCH 143/810] Add sats as a native currency This will allow you to create an invoice where its primary currency is denominated in sats --- .../Altcoins/BTCPayNetworkProvider.Bitcoin.cs | 5 +++++ .../Lightning/LightningLikePaymentHandler.cs | 1 + BTCPayServer/Services/Rates/CurrencyNameTable.cs | 15 +++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Bitcoin.cs b/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Bitcoin.cs index f03652ceb..10abe336a 100644 --- a/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Bitcoin.cs +++ b/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Bitcoin.cs @@ -21,6 +21,11 @@ namespace BTCPayServer UriScheme = "bitcoin", CryptoImagePath = "imlegacy/bitcoin.svg", LightningImagePath = "imlegacy/bitcoin-lightning.svg", + DefaultRateRules = new[] + { + "SATS_X = SATS_BTC * BTC_X", + "BTC_SATS = sats(BTC_SATS);", + }, DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'"), SupportRBF = true, diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index ca085c4f6..6668e2d17 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; diff --git a/BTCPayServer/Services/Rates/CurrencyNameTable.cs b/BTCPayServer/Services/Rates/CurrencyNameTable.cs index ee3a72a09..54098c094 100644 --- a/BTCPayServer/Services/Rates/CurrencyNameTable.cs +++ b/BTCPayServer/Services/Rates/CurrencyNameTable.cs @@ -98,6 +98,12 @@ namespace BTCPayServer.Services.Rates { AddCurrency(_CurrencyProviders, network.CryptoCode, 8, network.CryptoCode); } + + _CurrencyProviders.TryAdd("SATS", + new NumberFormatInfo() + { + CurrencySymbol = "sats", CurrencyDecimalDigits = 0, CurrencyPositivePattern = 3 + }); } return _CurrencyProviders.TryGet(currency.ToUpperInvariant()); } @@ -189,6 +195,15 @@ namespace BTCPayServer.Services.Rates } } + dico.TryAdd("SATS", new CurrencyData() + { + Code = "SATS", + Crypto = true, + Divisibility = 0, + Name = "Satoshis", + Symbol = "Sats", + }); + return dico.Values.ToArray(); } From 27a61b7afdc2ab3b544fbca1e55c5c3181125cc2 Mon Sep 17 00:00:00 2001 From: Kukks Date: Sun, 22 Sep 2019 13:46:02 +0200 Subject: [PATCH 144/810] fix test --- BTCPayServer.Tests/UnitTest1.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 3b6b0fcb3..1959cd912 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -2702,6 +2702,12 @@ noninventoryitem: Assert.Contains(exchangeRates.ByExchange[result.ExpectedName], e => e.CurrencyPair == new CurrencyPair("BTC", "JPY") && e.BidAsk.Bid > 100m); // 1BTC will always be more than 100JPY } + else if (result.ExpectedName == "sats") + { + Assert.Contains(exchangeRates.ByExchange[result.ExpectedName], + e => e.CurrencyPair == new CurrencyPair("SATS", "BTC") && + e.BidAsk.Bid == 0.00000001m); + } else { // This check if the currency pair is using right currency pair From f8139a9156170b3d3fa91c29370062ef9cdf4cd5 Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 23 Sep 2019 12:09:08 +0200 Subject: [PATCH 145/810] cleanup (remove sats rate provider and just use rate scripting) --- .../Altcoins/BTCPayNetworkProvider.Bitcoin.cs | 2 +- BTCPayServer.Tests/UnitTest1.cs | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Bitcoin.cs b/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Bitcoin.cs index 10abe336a..f1278b7b3 100644 --- a/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Bitcoin.cs +++ b/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Bitcoin.cs @@ -24,7 +24,7 @@ namespace BTCPayServer DefaultRateRules = new[] { "SATS_X = SATS_BTC * BTC_X", - "BTC_SATS = sats(BTC_SATS);", + "SATS_BTC = 0.00000001" }, DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'"), diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 1959cd912..3b6b0fcb3 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -2702,12 +2702,6 @@ noninventoryitem: Assert.Contains(exchangeRates.ByExchange[result.ExpectedName], e => e.CurrencyPair == new CurrencyPair("BTC", "JPY") && e.BidAsk.Bid > 100m); // 1BTC will always be more than 100JPY } - else if (result.ExpectedName == "sats") - { - Assert.Contains(exchangeRates.ByExchange[result.ExpectedName], - e => e.CurrencyPair == new CurrencyPair("SATS", "BTC") && - e.BidAsk.Bid == 0.00000001m); - } else { // This check if the currency pair is using right currency pair From a78dff5931c5872c3677503efb148b24652c4ba5 Mon Sep 17 00:00:00 2001 From: Kukks Date: Sun, 12 Jan 2020 13:53:31 +0100 Subject: [PATCH 146/810] remove padding --- BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index 6668e2d17..ca085c4f6 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; From f5cbf6672ab5533e000318221d1f9263eb4608da Mon Sep 17 00:00:00 2001 From: Kukks Date: Sun, 12 Jan 2020 13:53:48 +0100 Subject: [PATCH 147/810] remove default rate rule for sats --- .../Altcoins/BTCPayNetworkProvider.Bitcoin.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Bitcoin.cs b/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Bitcoin.cs index f1278b7b3..f03652ceb 100644 --- a/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Bitcoin.cs +++ b/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Bitcoin.cs @@ -21,11 +21,6 @@ namespace BTCPayServer UriScheme = "bitcoin", CryptoImagePath = "imlegacy/bitcoin.svg", LightningImagePath = "imlegacy/bitcoin-lightning.svg", - DefaultRateRules = new[] - { - "SATS_X = SATS_BTC * BTC_X", - "SATS_BTC = 0.00000001" - }, DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'"), SupportRBF = true, From a63502873ca12aa2c1764c3e58a6f6af433dc06b Mon Sep 17 00:00:00 2001 From: Kukks Date: Sun, 12 Jan 2020 13:54:06 +0100 Subject: [PATCH 148/810] Add implicit hidden rate rule for sats in parser --- BTCPayServer.Rating/RateRules.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/BTCPayServer.Rating/RateRules.cs b/BTCPayServer.Rating/RateRules.cs index 2b47b34fa..c91c90d77 100644 --- a/BTCPayServer.Rating/RateRules.cs +++ b/BTCPayServer.Rating/RateRules.cs @@ -99,6 +99,8 @@ namespace BTCPayServer.Rating RuleList ruleList; decimal _Spread; + private const string ImplicitSatsRule = "SATS_X = SATS_BTC * BTC_X;\nSATS_BTC = 0.00000001;\n"; + public decimal Spread { get @@ -126,6 +128,7 @@ namespace BTCPayServer.Rating } public static bool TryParse(string str, out RateRules rules, out List errors) { + str = ImplicitSatsRule + str; rules = null; errors = null; var expression = CSharpSyntaxTree.ParseText(str, new CSharpParseOptions(LanguageVersion.Default).WithKind(SourceCodeKind.Script)); @@ -195,6 +198,7 @@ namespace BTCPayServer.Rating { return root.NormalizeWhitespace("", "\n") .ToFullString() + .Replace(ImplicitSatsRule, string.Empty, StringComparison.OrdinalIgnoreCase) .Replace("{\n", string.Empty, StringComparison.OrdinalIgnoreCase) .Replace("\n}", string.Empty, StringComparison.OrdinalIgnoreCase); } From f324185d82cdc9b9c754f43ae9325a5762bd412a Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 21 Jan 2020 21:47:51 +0900 Subject: [PATCH 149/810] bump nbx --- BTCPayServer.Tests/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index fcf1ce112..f1010a58e 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -76,7 +76,7 @@ services: - customer_lnd - merchant_lnd nbxplorer: - image: nicolasdorier/nbxplorer:2.1.5 + image: nicolasdorier/nbxplorer:2.1.8 restart: unless-stopped ports: - "32838:32838" From 54c7c0d696691f2ee9790220a2a4547db2689b1b Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Tue, 21 Jan 2020 14:28:13 +0100 Subject: [PATCH 150/810] Add currency precision based on network (#1294) --- .../Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs | 10 +++++----- .../Altcoins/Monero/BTCPayNetworkProvider.Monero.cs | 1 + BTCPayServer.Common/BTCPayNetwork.cs | 6 +++--- .../Lightning/LightningLikePaymentHandler.cs | 2 +- BTCPayServer/Payments/PaymentTypes.cs | 1 - BTCPayServer/Services/Invoices/InvoiceEntity.cs | 12 ++++++------ BTCPayServer/Services/Rates/CurrencyNameTable.cs | 4 ++-- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs b/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs index dc0d789ed..8ab4dc883 100644 --- a/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs +++ b/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs @@ -6,16 +6,16 @@ using NBXplorer.Models; namespace BTCPayServer { - public class ElementsBTCPayNetwork:BTCPayNetwork + public class ElementsBTCPayNetwork : BTCPayNetwork { public string NetworkCryptoCode { get; set; } public uint256 AssetId { get; set; } public override bool ReadonlyWallet { get; set; } = true; - public int Divisibility { get; set; } = 8; - public override IEnumerable<(MatchedOutput matchedOutput, OutPoint outPoint)> GetValidOutputs(NewTransactionEvent evtOutputs) + public override IEnumerable<(MatchedOutput matchedOutput, OutPoint outPoint)> GetValidOutputs( + NewTransactionEvent evtOutputs) { - return evtOutputs.Outputs.Where(output => + return evtOutputs.Outputs.Where(output => output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId).Select(output => { var outpoint = new OutPoint(evtOutputs.TransactionData.TransactionHash, output.Index); @@ -23,7 +23,7 @@ namespace BTCPayServer }); } - public override string GenerateBIP21(string cryptoInfoAddress, string cryptoInfoDue) + public override string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue) { return $"{base.GenerateBIP21(cryptoInfoAddress, cryptoInfoDue)}&assetid={AssetId}"; } diff --git a/BTCPayServer.Common/Altcoins/Monero/BTCPayNetworkProvider.Monero.cs b/BTCPayServer.Common/Altcoins/Monero/BTCPayNetworkProvider.Monero.cs index f81731ff0..012c4bdd3 100644 --- a/BTCPayServer.Common/Altcoins/Monero/BTCPayNetworkProvider.Monero.cs +++ b/BTCPayServer.Common/Altcoins/Monero/BTCPayNetworkProvider.Monero.cs @@ -10,6 +10,7 @@ namespace BTCPayServer { CryptoCode = "XMR", DisplayName = "Monero", + Divisibility = 12, BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://www.exploremonero.com/transaction/{0}" diff --git a/BTCPayServer.Common/BTCPayNetwork.cs b/BTCPayServer.Common/BTCPayNetwork.cs index 2b67edfc8..6c71a8a2f 100644 --- a/BTCPayServer.Common/BTCPayNetwork.cs +++ b/BTCPayServer.Common/BTCPayNetwork.cs @@ -113,9 +113,9 @@ namespace BTCPayServer }); } - public virtual string GenerateBIP21(string cryptoInfoAddress, string cryptoInfoDue) + public virtual string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue) { - return $"{UriScheme}:{cryptoInfoAddress}?amount={cryptoInfoDue}"; + return $"{UriScheme}:{cryptoInfoAddress}?amount={cryptoInfoDue.ToString(false, true)}"; } } @@ -124,7 +124,7 @@ namespace BTCPayServer public string CryptoCode { get; internal set; } public string BlockExplorerLink { get; internal set; } public string DisplayName { get; set; } - + public int Divisibility { get; set; } = 8; [Obsolete("Should not be needed")] public bool IsBTC { diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index ca085c4f6..ea3546b6e 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -47,7 +47,7 @@ namespace BTCPayServer.Payments.Lightning var storeBlob = store.GetStoreBlob(); var test = GetNodeInfo(paymentMethod.PreferOnion, supportedPaymentMethod, (BTCPayNetwork)network); var invoice = paymentMethod.ParentEntity; - var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8); + var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, network.Divisibility); var client = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), (BTCPayNetwork)network); var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow; if (expiry < TimeSpan.Zero) diff --git a/BTCPayServer/Payments/PaymentTypes.cs b/BTCPayServer/Payments/PaymentTypes.cs index 524099274..bb5088bc9 100644 --- a/BTCPayServer/Payments/PaymentTypes.cs +++ b/BTCPayServer/Payments/PaymentTypes.cs @@ -64,7 +64,6 @@ namespace BTCPayServer.Payments public abstract string SerializePaymentData(BTCPayNetworkBase network, CryptoPaymentData paymentData); public abstract IPaymentMethodDetails DeserializePaymentMethodDetails(string str); public abstract ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value); - public abstract string GetTransactionLink(BTCPayNetworkBase network, string txId); public abstract string InvoiceViewPaymentPartialName { get; } } diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 181faa637..5576eed58 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -847,7 +847,7 @@ namespace BTCPayServer.Services.Invoices var paid = 0m; var cryptoPaid = 0.0m; - int precision = 8; + int precision = Network?.Divisibility ?? 8; var totalDueNoNetworkCost = Money.Coins(Extensions.RoundUp(totalDue, precision)); bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision); int txRequired = 0; @@ -857,8 +857,8 @@ namespace BTCPayServer.Services.Invoices .OrderBy(p => p.ReceivedTime) .Select(_ => { - var txFee = _.GetValue(paymentMethods, GetId(), _.NetworkFee); - paid += _.GetValue(paymentMethods, GetId()); + var txFee = _.GetValue(paymentMethods, GetId(), _.NetworkFee, precision); + paid += _.GetValue(paymentMethods, GetId(), null, precision); if (!paidEnough) { totalDue += txFee; @@ -991,18 +991,18 @@ namespace BTCPayServer.Services.Invoices #pragma warning restore CS0618 return this; } - internal decimal GetValue(PaymentMethodDictionary paymentMethods, PaymentMethodId paymentMethodId, decimal? value = null) + internal decimal GetValue(PaymentMethodDictionary paymentMethods, PaymentMethodId paymentMethodId, decimal? value, int precision) { value = value ?? this.GetCryptoPaymentData().GetValue(); var to = paymentMethodId; var from = this.GetPaymentMethodId(); if (to == from) - return decimal.Round(value.Value, 8); + return decimal.Round(value.Value, precision); var fromRate = paymentMethods[from].Rate; var toRate = paymentMethods[to].Rate; - var fiatValue = fromRate * decimal.Round(value.Value, 8); + var fiatValue = fromRate * decimal.Round(value.Value, precision); var otherCurrencyValue = toRate == 0 ? 0.0m : fiatValue / toRate; return otherCurrencyValue; } diff --git a/BTCPayServer/Services/Rates/CurrencyNameTable.cs b/BTCPayServer/Services/Rates/CurrencyNameTable.cs index ee3a72a09..78362bd34 100644 --- a/BTCPayServer/Services/Rates/CurrencyNameTable.cs +++ b/BTCPayServer/Services/Rates/CurrencyNameTable.cs @@ -96,7 +96,7 @@ namespace BTCPayServer.Services.Rates foreach (var network in new BTCPayNetworkProvider(NetworkType.Mainnet).GetAll()) { - AddCurrency(_CurrencyProviders, network.CryptoCode, 8, network.CryptoCode); + AddCurrency(_CurrencyProviders, network.CryptoCode, network.Divisibility, network.CryptoCode); } } return _CurrencyProviders.TryGet(currency.ToUpperInvariant()); @@ -180,7 +180,7 @@ namespace BTCPayServer.Services.Rates if (!dico.TryAdd(network.CryptoCode, new CurrencyData() { Code = network.CryptoCode, - Divisibility = 8, + Divisibility = network.Divisibility, Name = network.CryptoCode, Crypto = true })) From dcb3601791cdb3c6f33b740decafada3b9250ead Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Tue, 21 Jan 2020 18:22:42 +0100 Subject: [PATCH 151/810] Fix ETB asset id --- .../Altcoins/Liquid/BTCPayNetworkProvider.LiquidAssets.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.LiquidAssets.cs b/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.LiquidAssets.cs index 231af2278..aa6f35852 100644 --- a/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.LiquidAssets.cs +++ b/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.LiquidAssets.cs @@ -44,7 +44,7 @@ namespace BTCPayServer "ETB_BTC = bitpay(ETB_BTC)" }, Divisibility = 2, - AssetId = new uint256("ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"), + AssetId = new uint256("aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf"), DisplayName = "Ethiopian Birr", BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}", NBXplorerNetwork = nbxplorerNetwork, From 707669206907892bbb68b7a0eb92afe85c39eae8 Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Wed, 22 Jan 2020 07:16:32 +0100 Subject: [PATCH 152/810] fix configurator password loader (#1298) --- BTCPayServer/Configuration/ExternalConnectionString.cs | 2 +- BTCPayServer/Configuration/ExternalService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/BTCPayServer/Configuration/ExternalConnectionString.cs b/BTCPayServer/Configuration/ExternalConnectionString.cs index 2c0114138..1757c536f 100644 --- a/BTCPayServer/Configuration/ExternalConnectionString.cs +++ b/BTCPayServer/Configuration/ExternalConnectionString.cs @@ -95,7 +95,7 @@ namespace BTCPayServer.Configuration { throw new System.IO.FileNotFoundException("Cookie file path not found", ex); } - if (serviceType == ExternalServiceTypes.RTL) + if (serviceType == ExternalServiceTypes.RTL || serviceType == ExternalServiceTypes.Configurator) { connectionString.AccessKey = cookieFileContent; } diff --git a/BTCPayServer/Configuration/ExternalService.cs b/BTCPayServer/Configuration/ExternalService.cs index ccee3cb34..77775bd21 100644 --- a/BTCPayServer/Configuration/ExternalService.cs +++ b/BTCPayServer/Configuration/ExternalService.cs @@ -47,7 +47,7 @@ namespace BTCPayServer.Configuration public void LoadNonCryptoServices(IConfiguration configuration) { Load(configuration, null, "configurator", ExternalServiceTypes.Configurator, "Invalid setting {0}, " + Environment.NewLine + - $"configurator: 'passwordfile=/etc/configurator/password'" + Environment.NewLine + + $"configurator: 'cookiefilepathfile=/etc/configurator/cookie'" + Environment.NewLine + "Error: {1}", "Configurator"); } From b42e4f240acdcfa23ad2699c5f2061e1900554a4 Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Thu, 23 Jan 2020 14:02:37 +0100 Subject: [PATCH 153/810] Fix (#1301) * Fix seed signing validation * fix ident --- .../Controllers/WalletsController.PSBT.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/BTCPayServer/Controllers/WalletsController.PSBT.cs b/BTCPayServer/Controllers/WalletsController.PSBT.cs index 2c79f1d73..82efa784d 100644 --- a/BTCPayServer/Controllers/WalletsController.PSBT.cs +++ b/BTCPayServer/Controllers/WalletsController.PSBT.cs @@ -111,15 +111,18 @@ namespace BTCPayServer.Controllers case "seed": return SignWithSeed(walletId, psbt.ToBase64()); case "nbx-seed": - var derivationScheme = GetDerivationSchemeSettings(walletId); - var extKey = await ExplorerClientProvider.GetExplorerClient(network) - .GetMetadataAsync(derivationScheme.AccountDerivation, WellknownMetadataKeys.MasterHDKey); - - return await SignWithSeed(walletId, new SignWithSeedViewModel() + if (await CanUseHotWallet()) { - SeedOrKey = extKey, - PSBT = psbt.ToBase64() - }); + var derivationScheme = GetDerivationSchemeSettings(walletId); + var extKey = await ExplorerClientProvider.GetExplorerClient(network) + .GetMetadataAsync(derivationScheme.AccountDerivation, + WellknownMetadataKeys.MasterHDKey); + + return await SignWithSeed(walletId, + new SignWithSeedViewModel() {SeedOrKey = extKey, PSBT = psbt.ToBase64()}); + } + + return View(vm); case "broadcast": { return await WalletPSBTReady(walletId, psbt.ToBase64()); From 5dd3112e0dc7a03344a8f5b41b1fe85aa62305fb Mon Sep 17 00:00:00 2001 From: Umar Bolatov Date: Thu, 23 Jan 2020 05:20:00 -0800 Subject: [PATCH 154/810] Ensure "import from....a new/existing seed" modal text is readable in Casa theme (#1300) fix #1299 --- .../AddDerivationSchemes_NBXWalletGenerate.cshtml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/BTCPayServer/Views/Stores/AddDerivationSchemes_NBXWalletGenerate.cshtml b/BTCPayServer/Views/Stores/AddDerivationSchemes_NBXWalletGenerate.cshtml index ad5704730..61f2d5397 100644 --- a/BTCPayServer/Views/Stores/AddDerivationSchemes_NBXWalletGenerate.cshtml +++ b/BTCPayServer/Views/Stores/AddDerivationSchemes_NBXWalletGenerate.cshtml @@ -20,7 +20,9 @@ -

You can choose to import an existing mnemonic seed phrase. If you leave blank, we will generate one for you.

+ + You can choose to import an existing mnemonic seed phrase. If you leave blank, we will generate one for you. +
@@ -56,14 +58,17 @@ -

If checked, each private key associated with an address generated will be stored as metadata in NBXplorer. While convenient, this means that anyone with access to your server will have access to your private keys and will be able to steal your funds.

- + + If checked, each private key associated with an address generated will be stored as metadata in NBXplorer. While convenient, this means that anyone with access to your server will have access to your private keys and will be able to steal your funds. +
-

If checked, each address generated will be imported into the node wallet so that you can view your balance through your node. When this is enabled alongside Is hot wallet, you're also able to use the node wallet to spend (this works pretty well in conjunction with apps such as FullyNoded).

+ + If checked, each address generated will be imported into the node wallet so that you can view your balance through your node. When this is enabled alongside Is hot wallet, you're also able to use the node wallet to spend (this works pretty well in conjunction with apps such as FullyNoded). +
From 4ae173bb69f868a38f751203db73e9eb27e32e80 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Thu, 23 Jan 2020 20:01:49 -0600 Subject: [PATCH 157/810] Providing open in new window split button when updating POS app co-authored-by: radWorx --- BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml index 0e412913b..0f83d14e0 100644 --- a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml +++ b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml @@ -146,9 +146,17 @@
From a907143d81d8310b600a367bc68b50aa01d8244f Mon Sep 17 00:00:00 2001 From: rockstardev Date: Thu, 23 Jan 2020 20:17:29 -0600 Subject: [PATCH 158/810] Providing open in new window split button when updating crowdfund Unifying styles on POS and Crowdfund settings co-authored-by: radWorx --- BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml | 16 ++++++++++++---- BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml | 4 ++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml b/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml index f6ffa765c..8b27fe0de 100644 --- a/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml +++ b/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml @@ -216,12 +216,20 @@
- Invoices - @if ((Model.Description != null) && (Model.Title != null) && (Model.TargetCurrency != null)) +
+ Invoices + +
+ @if (Model.ModelWithMinimumData) { - +
+ View App + +
} - Back to the app list + Back to the app list
diff --git a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml index 0f83d14e0..dcd2281aa 100644 --- a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml +++ b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml @@ -152,8 +152,8 @@ target="viewinvoices_@Model.Id">
Back to the app list From f99058a9fad8a0b4b54089bf538fb5dcc7f0bd24 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Thu, 23 Jan 2020 20:18:33 -0600 Subject: [PATCH 159/810] Adding code comment for review --- BTCPayServer/Controllers/AppsController.Crowdfund.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BTCPayServer/Controllers/AppsController.Crowdfund.cs b/BTCPayServer/Controllers/AppsController.Crowdfund.cs index b742b85d9..29b4f0bcc 100644 --- a/BTCPayServer/Controllers/AppsController.Crowdfund.cs +++ b/BTCPayServer/Controllers/AppsController.Crowdfund.cs @@ -170,6 +170,8 @@ namespace BTCPayServer.Controllers TempData[WellKnownTempData.SuccessMessage] = "App updated"; return RedirectToAction(nameof(UpdateCrowdfund), new { appId }); } + // TODO: Check with Kukks if we can remove this tricky logic of redirecting to viewapp + // I've already switched do button directly linking to AppPublic ViewCrowdfund action else if (command == "viewapp") { return RedirectToAction(nameof(AppsPublicController.ViewCrowdfund), "AppsPublic", new { appId }); From a83edce4dc09331fd1c7d9038e7562b876f19098 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Thu, 23 Jan 2020 20:19:24 -0600 Subject: [PATCH 160/810] Updating idents, code formatting --- .../Controllers/AppsController.Crowdfund.cs | 22 ++++++------- .../Controllers/AppsController.PointOfSale.cs | 16 ++++----- .../AppViewModels/UpdateCrowdfundViewModel.cs | 33 +++++++++++++------ .../Views/Apps/UpdateCrowdfund.cshtml | 15 ++++----- .../Views/Apps/UpdatePointOfSale.cshtml | 13 ++++---- .../Views/AppsPublic/ViewCrowdfund.cshtml | 30 ++++++++--------- 6 files changed, 70 insertions(+), 59 deletions(-) diff --git a/BTCPayServer/Controllers/AppsController.Crowdfund.cs b/BTCPayServer/Controllers/AppsController.Crowdfund.cs index 29b4f0bcc..503e37c91 100644 --- a/BTCPayServer/Controllers/AppsController.Crowdfund.cs +++ b/BTCPayServer/Controllers/AppsController.Crowdfund.cs @@ -22,8 +22,8 @@ namespace BTCPayServer.Controllers return String.Empty; } } - - + + [HttpGet] [Route("{appId}/settings/crowdfund")] public async Task UpdateCrowdfund(string appId) @@ -61,8 +61,8 @@ namespace BTCPayServer.Controllers SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetCrowdfundOrderId(appId)}", DisplayPerksRanking = settings.DisplayPerksRanking, SortPerksByPopularity = settings.SortPerksByPopularity, - Sounds = string.Join(Environment.NewLine, settings.Sounds), - AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors) + Sounds = string.Join(Environment.NewLine, settings.Sounds), + AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors) }; return View(vm); } @@ -70,9 +70,9 @@ namespace BTCPayServer.Controllers [Route("{appId}/settings/crowdfund")] public async Task UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm, string command) { - if (!string.IsNullOrEmpty( vm.TargetCurrency) && _currencies.GetCurrencyData(vm.TargetCurrency, false) == null) + if (!string.IsNullOrEmpty(vm.TargetCurrency) && _currencies.GetCurrencyData(vm.TargetCurrency, false) == null) ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency"); - + try { _AppService.Parse(vm.PerksTemplate, vm.TargetCurrency).ToString(); @@ -98,14 +98,14 @@ namespace BTCPayServer.Controllers } var parsedSounds = vm.Sounds.Split( - new[] {"\r\n", "\r", "\n"}, + new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None ).Select(s => s.Trim()).ToArray(); if (vm.SoundsEnabled && !parsedSounds.Any()) { ModelState.AddModelError(nameof(vm.Sounds), "You must have at least one sound if you enable sounds"); } - + var parsedAnimationColors = vm.AnimationColors.Split( new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None @@ -114,13 +114,13 @@ namespace BTCPayServer.Controllers { ModelState.AddModelError(nameof(vm.AnimationColors), "You must have at least one animation color if you enable animations"); } - + if (!ModelState.IsValid) { return View(vm); } - - + + var app = await GetOwnedApp(appId, AppType.Crowdfund); if (app == null) return NotFound(); diff --git a/BTCPayServer/Controllers/AppsController.PointOfSale.cs b/BTCPayServer/Controllers/AppsController.PointOfSale.cs index 51032b52f..53c6eadff 100644 --- a/BTCPayServer/Controllers/AppsController.PointOfSale.cs +++ b/BTCPayServer/Controllers/AppsController.PointOfSale.cs @@ -79,9 +79,9 @@ namespace BTCPayServer.Controllers public string CustomCSSLink { get; set; } - + public string EmbeddedCSS { get; set; } - + public string Description { get; set; } public string NotificationEmail { get; set; } public string NotificationUrl { get; set; } @@ -96,7 +96,7 @@ namespace BTCPayServer.Controllers if (app == null) return NotFound(); var settings = app.GetSettings(); - + var vm = new UpdatePointOfSaleViewModel() { NotificationEmailWarning = !await IsEmailConfigured(app.StoreDataId), @@ -119,7 +119,7 @@ namespace BTCPayServer.Controllers NotificationEmail = settings.NotificationEmail, NotificationUrl = settings.NotificationUrl, SearchTerm = $"storeid:{app.StoreDataId}", - RedirectAutomatically = settings.RedirectAutomatically.HasValue? settings.RedirectAutomatically.Value? "true": "false" : "" + RedirectAutomatically = settings.RedirectAutomatically.HasValue ? settings.RedirectAutomatically.Value ? "true" : "false" : "" }; if (HttpContext?.Request != null) { @@ -197,8 +197,8 @@ namespace BTCPayServer.Controllers NotificationEmail = vm.NotificationEmail, Description = vm.Description, EmbeddedCSS = vm.EmbeddedCSS, - RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically)? (bool?) null: bool.Parse(vm.RedirectAutomatically) - + RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? (bool?)null : bool.Parse(vm.RedirectAutomatically) + }); await _AppService.UpdateOrCreateApp(app); TempData[WellKnownTempData.SuccessMessage] = "App updated"; @@ -211,8 +211,8 @@ namespace BTCPayServer.Controllers if (string.IsNullOrEmpty(list)) { return Array.Empty(); - } - else + } + else { // Remove all characters except numeric and comma Regex charsToDestroy = new Regex(@"[^\d|\" + separator + "]"); diff --git a/BTCPayServer/Models/AppViewModels/UpdateCrowdfundViewModel.cs b/BTCPayServer/Models/AppViewModels/UpdateCrowdfundViewModel.cs index f3fe5ae50..7cf676689 100644 --- a/BTCPayServer/Models/AppViewModels/UpdateCrowdfundViewModel.cs +++ b/BTCPayServer/Models/AppViewModels/UpdateCrowdfundViewModel.cs @@ -10,16 +10,20 @@ namespace BTCPayServer.Models.AppViewModels public class UpdateCrowdfundViewModel { public string StoreId { get; set; } - [Required] [MaxLength(30)] public string Title { get; set; } + [Required] + [MaxLength(30)] + public string Title { get; set; } - [MaxLength(50)] public string Tagline { get; set; } + [MaxLength(50)] + public string Tagline { get; set; } + + [Required] + public string Description { get; set; } - [Required] public string Description { get; set; } - [Display(Name = "Featured Image")] public string MainImageUrl { get; set; } - - [Display(Name = "Callback Notification Url")] + + [Display(Name = "Callback Notification Url")] [Uri] public string NotificationUrl { get; set; } [Display(Name = "Invoice IPN Notification")] @@ -59,7 +63,8 @@ namespace BTCPayServer.Models.AppViewModels public IEnumerable ResetEveryValues = Enum.GetNames(typeof(CrowdfundResetEvery)); - [Display(Name = "Reset goal every")] public string ResetEvery { get; set; } = nameof(CrowdfundResetEvery.Never); + [Display(Name = "Reset goal every")] + public string ResetEvery { get; set; } = nameof(CrowdfundResetEvery.Never); public int ResetEveryAmount { get; set; } = 1; @@ -78,7 +83,7 @@ namespace BTCPayServer.Models.AppViewModels public string EmbeddedCSS { get; set; } [Display(Name = "Count all invoices created on the store as part of the crowdfunding goal")] - public bool UseAllStoreInvoices { get; set; } + public bool UseAllStoreInvoices { get; set; } public string AppId { get; set; } public string SearchTerm { get; set; } @@ -90,10 +95,18 @@ namespace BTCPayServer.Models.AppViewModels [Display(Name = "Sounds to play when a payment is made. One sound per line")] - public string Sounds{ get; set; } + public string Sounds { get; set; } [Display(Name = "Colors to rotate between with animation when a payment is made. First color is the default background. One color per line. Can be any valid css color value.")] - public string AnimationColors{ get; set; } + public string AnimationColors { get; set; } public bool NotificationEmailWarning { get; set; } + + + // NOTE: Improve validation if needed + public bool ModelWithMinimumData + { + get { return Description != null && Title != null && TargetCurrency != null; } + } + } } diff --git a/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml b/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml index 8b27fe0de..d4f416aa2 100644 --- a/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml +++ b/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml @@ -263,13 +263,12 @@
* - +
@@ -293,7 +292,7 @@
- +
diff --git a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml index dcd2281aa..4649a1cc9 100644 --- a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml +++ b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml @@ -261,12 +261,11 @@
* - +
@@ -290,7 +289,7 @@
- +
diff --git a/BTCPayServer/Views/AppsPublic/ViewCrowdfund.cshtml b/BTCPayServer/Views/AppsPublic/ViewCrowdfund.cshtml index 98fada78c..9ab010371 100644 --- a/BTCPayServer/Views/AppsPublic/ViewCrowdfund.cshtml +++ b/BTCPayServer/Views/AppsPublic/ViewCrowdfund.cshtml @@ -33,27 +33,27 @@ @if (!string.IsNullOrEmpty(Model.EmbeddedCSS)) { - @Safe.Raw($""); + @Safe.Raw($""); } -@if (Context.Request.Query.ContainsKey("simple")) -{ - @await Html.PartialAsync("/Views/AppsPublic/Crowdfund/MinimalCrowdfund.cshtml", Model) -} -else -{ - - - if (Model.AnimationsEnabled) + @if (Context.Request.Query.ContainsKey("simple")) { - + @await Html.PartialAsync("/Views/AppsPublic/Crowdfund/MinimalCrowdfund.cshtml", Model) + } + else + { + + + if (Model.AnimationsEnabled) + { + + } + @await Html.PartialAsync("/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml", Model) } - @await Html.PartialAsync("/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml", Model) -} From c7c0db612a468188acbe1bd01b5b077def4cd379 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Thu, 23 Jan 2020 20:39:11 -0600 Subject: [PATCH 161/810] Restoring IDs Selenium depends on for tests --- BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml | 2 +- BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml b/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml index d4f416aa2..b9ed7b0ec 100644 --- a/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml +++ b/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml @@ -224,7 +224,7 @@ @if (Model.ModelWithMinimumData) { diff --git a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml index 4649a1cc9..ea66a04f5 100644 --- a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml +++ b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml @@ -152,7 +152,7 @@ target="viewinvoices_@Model.Id"> From 652b958d4f20287da969d693687dd35a46e0fd3c Mon Sep 17 00:00:00 2001 From: rockstardev Date: Fri, 24 Jan 2020 15:11:34 -0600 Subject: [PATCH 162/810] Removing viewapp command now that we directly redirect in cshtml --- .../Controllers/AppsController.Crowdfund.cs | 26 ++++++------------- .../Views/Apps/UpdateCrowdfund.cshtml | 2 +- .../Views/Apps/UpdatePointOfSale.cshtml | 2 +- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/BTCPayServer/Controllers/AppsController.Crowdfund.cs b/BTCPayServer/Controllers/AppsController.Crowdfund.cs index 503e37c91..98d40b0a6 100644 --- a/BTCPayServer/Controllers/AppsController.Crowdfund.cs +++ b/BTCPayServer/Controllers/AppsController.Crowdfund.cs @@ -157,26 +157,16 @@ namespace BTCPayServer.Controllers app.TagAllInvoices = vm.UseAllStoreInvoices; app.SetSettings(newSettings); - if (command == "save") - { - await _AppService.UpdateOrCreateApp(app); + await _AppService.UpdateOrCreateApp(app); - _EventAggregator.Publish(new AppUpdated() - { - AppId = appId, - StoreId = app.StoreDataId, - Settings = newSettings - }); - TempData[WellKnownTempData.SuccessMessage] = "App updated"; - return RedirectToAction(nameof(UpdateCrowdfund), new { appId }); - } - // TODO: Check with Kukks if we can remove this tricky logic of redirecting to viewapp - // I've already switched do button directly linking to AppPublic ViewCrowdfund action - else if (command == "viewapp") + _EventAggregator.Publish(new AppUpdated() { - return RedirectToAction(nameof(AppsPublicController.ViewCrowdfund), "AppsPublic", new { appId }); - } - return NotFound(); + AppId = appId, + StoreId = app.StoreDataId, + Settings = newSettings + }); + TempData[WellKnownTempData.SuccessMessage] = "App updated"; + return RedirectToAction(nameof(UpdateCrowdfund), new { appId }); } } } diff --git a/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml b/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml index b9ed7b0ec..12a5091af 100644 --- a/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml +++ b/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml @@ -215,7 +215,7 @@
- +
Invoices
- +
Invoices Date: Fri, 24 Jan 2020 17:29:52 +1000 Subject: [PATCH 163/810] Update display text on the view model. --- .../Models/InvoicingModels/CreateInvoiceModel.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs b/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs index b9b0b2dba..0b8e3c893 100644 --- a/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs +++ b/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs @@ -15,6 +15,7 @@ namespace BTCPayServer.Models.InvoicingModels { Currency = "USD"; } + [Required] public decimal? Amount { @@ -28,11 +29,13 @@ namespace BTCPayServer.Models.InvoicingModels } [Required] + [DisplayName("Store Id")] public string StoreId { get; set; } + [DisplayName("Order Id")] public string OrderId { get; set; @@ -51,6 +54,7 @@ namespace BTCPayServer.Models.InvoicingModels } [EmailAddress] + [DisplayName("Buyer Email")] public string BuyerEmail { get; set; @@ -72,22 +76,19 @@ namespace BTCPayServer.Models.InvoicingModels public SelectList Stores { - get; - set; + get; set; } [DisplayName("Supported Transaction Currencies")] public List SupportedTransactionCurrencies { - get; - set; + get; set; } [DisplayName("Available Payment Methods")] public SelectList AvailablePaymentMethods { - get; - set; + get; set; } } } From e00136de93625a9f7fa42b5256ffb03cc7380804 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sun, 26 Jan 2020 15:02:40 +0900 Subject: [PATCH 164/810] Fix spurious DefaultAntiforgery errors --- BTCPayServer/Hosting/Startup.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index 3aac9ae7e..541f5cb3f 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Hosting; using OpenIddict.Validation.AspNetCore; using OpenIddict.Abstractions; @@ -49,6 +50,9 @@ namespace BTCPayServer.Hosting Logs.Configure(LoggerFactory); services.ConfigureBTCPayServer(Configuration); services.AddMemoryCache(); + services.AddDataProtection() + .SetApplicationName("BTCPay Server") + .PersistKeysToFileSystem(GetDataDir()); services.AddIdentity() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); @@ -140,6 +144,11 @@ namespace BTCPayServer.Hosting } } + private DirectoryInfo GetDataDir() + { + return new DirectoryInfo(Configuration.GetDataDir(DefaultConfiguration.GetNetworkType(Configuration))); + } + private void ConfigureOpenIddict(IServiceCollection services) { // Register the OpenIddict services. From 06f1c17a5fad6bd9ac599444731eda0459bae234 Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Sun, 26 Jan 2020 11:45:24 +0100 Subject: [PATCH 165/810] Make unused assets in store settings collapsed (#1310) --- BTCPayServer/Controllers/StoresController.cs | 7 +++++-- .../Models/StoreViewModels/StoreViewModel.cs | 1 + BTCPayServer/Views/Stores/UpdateStore.cshtml | 21 +++++++++++++++++-- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index ff7f3bd1f..5303549b4 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -499,13 +499,16 @@ namespace BTCPayServer.Controllers case BitcoinPaymentType _: var strategy = derivationByCryptoCode.TryGet(paymentMethodId.CryptoCode); var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode); + var value = strategy?.ToPrettyString() ?? string.Empty; + vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme() { Crypto = paymentMethodId.CryptoCode, WalletSupported = network.WalletSupported, - Value = strategy?.ToPrettyString() ?? string.Empty, + Value = value, WalletId = new WalletId(store.Id, paymentMethodId.CryptoCode), - Enabled = !excludeFilters.Match(paymentMethodId) && strategy != null + Enabled = !excludeFilters.Match(paymentMethodId) && strategy != null, + Collapsed = network is ElementsBTCPayNetwork elementsBTCPayNetwork && elementsBTCPayNetwork.NetworkCryptoCode != elementsBTCPayNetwork.CryptoCode && string.IsNullOrEmpty(value) }); break; case LightningPaymentType _: diff --git a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs index db3764b4a..6475b4d15 100644 --- a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs @@ -14,6 +14,7 @@ namespace BTCPayServer.Models.StoreViewModels public WalletId WalletId { get; set; } public bool WalletSupported { get; set; } public bool Enabled { get; set; } + public bool Collapsed { get; set; } } public class AdditionalPaymentMethod diff --git a/BTCPayServer/Views/Stores/UpdateStore.cshtml b/BTCPayServer/Views/Stores/UpdateStore.cshtml index fd147ad4b..f7f91ba78 100644 --- a/BTCPayServer/Views/Stores/UpdateStore.cshtml +++ b/BTCPayServer/Views/Stores/UpdateStore.cshtml @@ -97,9 +97,9 @@
- @foreach(var scheme in Model.DerivationSchemes) + @foreach(var scheme in Model.DerivationSchemes.OrderBy(scheme => scheme.Collapsed)) { - + } + + @if (Model.DerivationSchemes.Any(scheme => scheme.Collapsed)) + { + + + + }
Payment methodAddress Rate Paid Due
@payment.PaymentMethod - @payment.Address - @payment.Rate @payment.Paid @payment.Due
@s.CryptoCode
@s.DisplayName + See information +
@if (app.IsOwner) { - Settings - + Settings + - } - View - + View + + - Remove
@scheme.Crypto @scheme.Value @@ -121,6 +121,13 @@
@@ -250,4 +257,14 @@ @section Scripts { @await Html.PartialAsync("_ValidationScriptsPartial") + + } From e058903450d8815e487bb424b22f2269b05901bc Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Sun, 26 Jan 2020 11:45:52 +0100 Subject: [PATCH 166/810] Do not show assets in sync modal (#1309) --- .../Liquid/BTCPayNetworkProvider.LiquidAssets.cs | 9 +++------ BTCPayServer.Common/BTCPayNetwork.cs | 1 + BTCPayServer/Views/Shared/SyncModal.cshtml | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.LiquidAssets.cs b/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.LiquidAssets.cs index aa6f35852..b5fde6acc 100644 --- a/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.LiquidAssets.cs +++ b/BTCPayServer.Common/Altcoins/Liquid/BTCPayNetworkProvider.LiquidAssets.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using NBitcoin; -using NBXplorer; +using NBitcoin; namespace BTCPayServer { @@ -16,6 +11,7 @@ namespace BTCPayServer { CryptoCode = "USDt", NetworkCryptoCode = "LBTC", + ShowSyncSummary = false, DefaultRateRules = new[] { "USDT_UST = 1", @@ -37,6 +33,7 @@ namespace BTCPayServer { CryptoCode = "ETB", NetworkCryptoCode = "LBTC", + ShowSyncSummary = false, DefaultRateRules = new[] { diff --git a/BTCPayServer.Common/BTCPayNetwork.cs b/BTCPayServer.Common/BTCPayNetwork.cs index 6c71a8a2f..c95b58b3c 100644 --- a/BTCPayServer.Common/BTCPayNetwork.cs +++ b/BTCPayServer.Common/BTCPayNetwork.cs @@ -121,6 +121,7 @@ namespace BTCPayServer public abstract class BTCPayNetworkBase { + public bool ShowSyncSummary { get; set; } = true; public string CryptoCode { get; internal set; } public string BlockExplorerLink { get; internal set; } public string DisplayName { get; set; } diff --git a/BTCPayServer/Views/Shared/SyncModal.cshtml b/BTCPayServer/Views/Shared/SyncModal.cshtml index eb5e4c906..2e2d4e11d 100644 --- a/BTCPayServer/Views/Shared/SyncModal.cshtml +++ b/BTCPayServer/Views/Shared/SyncModal.cshtml @@ -14,7 +14,7 @@

Your node is synching the entire blockchain and validating the consensus rules...

- @foreach (var line in dashboard.GetAll()) + @foreach (var line in dashboard.GetAll().Where(summary => summary.Network.ShowSyncSummary)) {

@line.Network.CryptoCode

@if (line.Status == null) From bb12d37416ddb58def44ced0c3eeb6e14f848050 Mon Sep 17 00:00:00 2001 From: rockstardev <5191402+rockstardev@users.noreply.github.com> Date: Mon, 27 Jan 2020 04:57:46 -0600 Subject: [PATCH 167/810] Displaying sats in a more user-friendly way (space as group separator) (#1306) Fix: #1146 --- .../Payments/Lightning/LightningLikePaymentHandler.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index ea3546b6e..9b27f0562 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -180,11 +180,15 @@ namespace BTCPayServer.Payments.Lightning model.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi; if (storeBlob.LightningAmountInSatoshi && model.CryptoCode == "BTC" ) { + var satoshiCulture = new CultureInfo(CultureInfo.InvariantCulture.Name); + satoshiCulture.NumberFormat.NumberGroupSeparator = " "; + model.CryptoCode = "Sats"; - model.BtcDue = Money.Parse(model.BtcDue).ToUnit(MoneyUnit.Satoshi).ToString(CultureInfo.InvariantCulture); - model.BtcPaid = Money.Parse(model.BtcPaid).ToUnit(MoneyUnit.Satoshi).ToString(CultureInfo.InvariantCulture); + model.BtcDue = Money.Parse(model.BtcDue).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture); + model.BtcPaid = Money.Parse(model.BtcPaid).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture); + model.OrderAmount = Money.Parse(model.OrderAmount).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture); + model.NetworkFee = new Money(model.NetworkFee, MoneyUnit.BTC).ToUnit(MoneyUnit.Satoshi); - model.OrderAmount = Money.Parse(model.OrderAmount).ToUnit(MoneyUnit.Satoshi).ToString(CultureInfo.InvariantCulture); } } public override string GetCryptoImage(PaymentMethodId paymentMethodId) From 23aaf794ef0e97dd2aad935cfee49fa7c7d9d5fa Mon Sep 17 00:00:00 2001 From: Umar Bolatov Date: Sat, 25 Jan 2020 20:43:48 -0800 Subject: [PATCH 168/810] Add nullable enable directive to HttpClientRequestMaker.MakeRequestAsync --- BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs b/BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs index 17fccf327..94fd517f4 100644 --- a/BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs +++ b/BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs @@ -196,6 +196,7 @@ namespace BTCPayServer.Services.Rates throw new APIException(text); } api.ProcessResponse(new InternalHttpWebResponse(webHttpResponse)); + #nullable enable Action? requestStateChanged = RequestStateChanged; if (requestStateChanged != null) { From 00747906849f093712c3907c99404c55b3defa66 Mon Sep 17 00:00:00 2001 From: Umar Bolatov Date: Mon, 27 Jan 2020 19:33:26 -0800 Subject: [PATCH 169/810] Remove "#nullable enable" directive and unnecessary operators --- BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs b/BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs index 94fd517f4..f5b03d3f7 100644 --- a/BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs +++ b/BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs @@ -196,11 +196,9 @@ namespace BTCPayServer.Services.Rates throw new APIException(text); } api.ProcessResponse(new InternalHttpWebResponse(webHttpResponse)); - #nullable enable - Action? requestStateChanged = RequestStateChanged; - if (requestStateChanged != null) + if (RequestStateChanged != null) { - requestStateChanged!(this, RequestMakerState.Finished, text); + RequestStateChanged(this, RequestMakerState.Finished, text); return text; } return text; From a08d5be35c654dd2338cb6003afc1706a958bb74 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Wed, 29 Jan 2020 22:31:43 -0600 Subject: [PATCH 170/810] Expanding tests to check implicit conversion of Sats to BTC --- BTCPayServer.Tests/RateRulesTest.cs | 35 +++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/BTCPayServer.Tests/RateRulesTest.cs b/BTCPayServer.Tests/RateRulesTest.cs index 8a508cede..66fd9bade 100644 --- a/BTCPayServer.Tests/RateRulesTest.cs +++ b/BTCPayServer.Tests/RateRulesTest.cs @@ -56,6 +56,8 @@ namespace BTCPayServer.Tests (Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)"), (Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1"), (Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02"), + (Pair: "SATS_CAD", Expected: "0.00000001 * coinbase(BTC_CAD)"), + (Pair: "Sats_USD", Expected: "0.00000001 * kraken(BTC_USD)") }; foreach (var test in tests) { @@ -102,6 +104,8 @@ namespace BTCPayServer.Tests (Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)", ExpectedExchangeRates: "coinbase(BTC_CAD)"), (Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),coinbase(BTC_CAD)"), (Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02", ExpectedExchangeRates: "coinaverage(LTC_CAD)"), + (Pair: "SATS_USD", Expected: "0.00000001 * kraken(BTC_USD)", ExpectedExchangeRates: "kraken(BTC_USD)"), + (Pair: "SATS_EUR", Expected: "0.00000001 * coinbase(BTC_EUR)", ExpectedExchangeRates: "coinbase(BTC_EUR)") }; foreach (var test in tests2) { @@ -189,6 +193,37 @@ namespace BTCPayServer.Tests rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m)); Assert.True(rule2.Reevaluate()); Assert.Equal($"({(1m / 6100m).ToString(CultureInfo.InvariantCulture)}, {(1m / 6000m).ToString(CultureInfo.InvariantCulture)})", rule2.ToString(true)); + + // Make sure defining value in sats works + builder = new StringBuilder(); + builder.AppendLine("BTC_USD = kraken(BTC_USD)"); + builder.AppendLine("BTC_X = coinbase(BTC_X)"); + Assert.True(RateRules.TryParse(builder.ToString(), out rules)); + rule2 = rules.GetRuleFor(CurrencyPair.Parse("SATS_USD")); + rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m)); + Assert.True(rule2.Reevaluate()); + Assert.Equal("0.00000001 * (6000, 6100)", rule2.ToString(true)); + Assert.Equal(0.00006m, rule2.BidAsk.Bid); + rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_SATS")); + rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m)); + Assert.True(rule2.Reevaluate()); + Assert.Equal("1 / (0.00000001 * (6000, 6100))", rule2.ToString(true)); + Assert.Equal(1m / 0.000061m, rule2.BidAsk.Bid); + + // testing rounding + rule2 = rules.GetRuleFor(CurrencyPair.Parse("Sats_EUR")); + rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_EUR"), new BidAsk(1.23m, 2.34m)); + Assert.True(rule2.Reevaluate()); + Assert.Equal("0.00000001 * (1.23, 2.34)", rule2.ToString(true)); + Assert.Equal(0.0000000234m, rule2.BidAsk.Ask); + Assert.Equal(0.0000000123m, rule2.BidAsk.Bid); + + rule2 = rules.GetRuleFor(CurrencyPair.Parse("EUR_Sats")); + rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_EUR"), new BidAsk(1.23m, 2.34m)); + Assert.True(rule2.Reevaluate()); + Assert.Equal("1 / (0.00000001 * (1.23, 2.34))", rule2.ToString(true)); + Assert.Equal(1m / 0.0000000123m, rule2.BidAsk.Ask); + Assert.Equal(1m / 0.0000000234m, rule2.BidAsk.Bid); } } } From 876c940032b564b4052b2b20c646f7a387bdd50d Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sat, 1 Feb 2020 00:26:01 -0600 Subject: [PATCH 171/810] Reverting delegate reference to previous state until Nicolas confirms change --- BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs b/BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs index f5b03d3f7..88a5e281e 100644 --- a/BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs +++ b/BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs @@ -196,9 +196,12 @@ namespace BTCPayServer.Services.Rates throw new APIException(text); } api.ProcessResponse(new InternalHttpWebResponse(webHttpResponse)); - if (RequestStateChanged != null) + // local reference to handle delegate becoming null, extended discussion here: + // https://github.com/btcpayserver/btcpayserver/commit/00747906849f093712c3907c99404c55b3defa66#r37022103 + var requestStateChanged = RequestStateChanged; + if (requestStateChanged != null) { - RequestStateChanged(this, RequestMakerState.Finished, text); + requestStateChanged(this, RequestMakerState.Finished, text); return text; } return text; From 88835b5b554692e3c4dfe69157682149d2418f1d Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sat, 1 Feb 2020 00:32:31 -0600 Subject: [PATCH 172/810] Moving _LayoutWelcome to shared folder --- BTCPayServer/Views/Account/Login.cshtml | 2 +- BTCPayServer/Views/Account/Register.cshtml | 2 +- .../_WelcomeLayout.cshtml => Shared/_LayoutWelcome.cshtml} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename BTCPayServer/Views/{Account/_WelcomeLayout.cshtml => Shared/_LayoutWelcome.cshtml} (100%) diff --git a/BTCPayServer/Views/Account/Login.cshtml b/BTCPayServer/Views/Account/Login.cshtml index 2fc0c4d41..b7eaadeca 100644 --- a/BTCPayServer/Views/Account/Login.cshtml +++ b/BTCPayServer/Views/Account/Login.cshtml @@ -2,7 +2,7 @@ @inject BTCPayServer.HostedServices.CssThemeManager themeManager @{ ViewData["Title"] = "Log in"; - Layout = "_WelcomeLayout.cshtml"; + Layout = "_LayoutWelcome"; } @if (TempData.HasStatusMessage()) { diff --git a/BTCPayServer/Views/Account/Register.cshtml b/BTCPayServer/Views/Account/Register.cshtml index a7b7e298c..b87603b1b 100644 --- a/BTCPayServer/Views/Account/Register.cshtml +++ b/BTCPayServer/Views/Account/Register.cshtml @@ -2,7 +2,7 @@ @{ ViewData["Title"] = "Register"; var useBasicLayout = ViewData["UseBasicLayout"] is true; - Layout = useBasicLayout ? "../Shared/_Layout.cshtml" : "_WelcomeLayout.cshtml"; + Layout = useBasicLayout ? "_Layout" : "_LayoutWelcome"; } @if (TempData.HasStatusMessage()) { diff --git a/BTCPayServer/Views/Account/_WelcomeLayout.cshtml b/BTCPayServer/Views/Shared/_LayoutWelcome.cshtml similarity index 100% rename from BTCPayServer/Views/Account/_WelcomeLayout.cshtml rename to BTCPayServer/Views/Shared/_LayoutWelcome.cshtml From fe2eca4fda72884470c0ff98ba27a52ddc04b071 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sat, 1 Feb 2020 01:40:50 -0600 Subject: [PATCH 173/810] Adding prettier error handling page in the pipeline --- BTCPayServer/Controllers/ErrorController.cs | 18 +++ BTCPayServer/Hosting/Startup.cs | 5 +- BTCPayServer/Views/Error/Handle.cshtml | 19 +++ BTCPayServer/Views/Error/_LayoutError.cshtml | 118 ++++++++++++++++++ BTCPayServer/Views/Error/_ViewStart.cshtml | 3 + .../Views/Shared/_BTCPaySupporters.cshtml | 41 ++++++ .../Views/Shared/_LayoutWelcome.cshtml | 42 +------ 7 files changed, 204 insertions(+), 42 deletions(-) create mode 100644 BTCPayServer/Controllers/ErrorController.cs create mode 100644 BTCPayServer/Views/Error/Handle.cshtml create mode 100644 BTCPayServer/Views/Error/_LayoutError.cshtml create mode 100644 BTCPayServer/Views/Error/_ViewStart.cshtml create mode 100644 BTCPayServer/Views/Shared/_BTCPaySupporters.cshtml diff --git a/BTCPayServer/Controllers/ErrorController.cs b/BTCPayServer/Controllers/ErrorController.cs new file mode 100644 index 000000000..29a02b9e7 --- /dev/null +++ b/BTCPayServer/Controllers/ErrorController.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Controllers +{ + [Route("[controller]/[action]")] + public class ErrorController : Controller + { + public IActionResult Handle(int? statusCode = null) + { + return View(statusCode); + } + } +} diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index 541f5cb3f..a88e7f037 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -241,6 +241,10 @@ namespace BTCPayServer.Hosting forwardingOptions.KnownProxies.Clear(); forwardingOptions.ForwardedHeaders = ForwardedHeaders.All; app.UseForwardedHeaders(forwardingOptions); + + + app.UseStatusCodePagesWithReExecute("/Error/Handle", "?statusCode={0}"); + app.UsePayServer(); app.UseRouting(); app.UseCors(); @@ -252,7 +256,6 @@ namespace BTCPayServer.Hosting app.UseSession(); app.UseWebSockets(); - app.UseStatusCodePages(); app.UseEndpoints(endpoints => { diff --git a/BTCPayServer/Views/Error/Handle.cshtml b/BTCPayServer/Views/Error/Handle.cshtml new file mode 100644 index 000000000..f14d7711a --- /dev/null +++ b/BTCPayServer/Views/Error/Handle.cshtml @@ -0,0 +1,19 @@ +@using System.Net +@model int? +@{ + ViewData["ErrorTitle"] = "Generic Error occurred"; + if (Model.HasValue) + { + var httpCode = (HttpStatusCode)Model.Value; + ViewData["ErrorTitle"] = $"{(int)httpCode} - {httpCode.ToString()}"; + } +} + +
diff --git a/BTCPayServer/Views/Error/_LayoutError.cshtml b/BTCPayServer/Views/Error/_LayoutError.cshtml new file mode 100644 index 000000000..d208baafc --- /dev/null +++ b/BTCPayServer/Views/Error/_LayoutError.cshtml @@ -0,0 +1,118 @@ +@{ + Layout = null; +} +@inject BTCPayServer.Services.BTCPayServerEnvironment env + + + + + + + + + +
+
+ + +
+
+
+ +

@ViewData["ErrorTitle"]

+
+
+
+
+
+ @RenderBody() +
+
+
+
+
+ @await Html.PartialAsync("_BTCPaySupporters") +
+
+
+
+
+ + diff --git a/BTCPayServer/Views/Error/_ViewStart.cshtml b/BTCPayServer/Views/Error/_ViewStart.cshtml new file mode 100644 index 000000000..5d3510360 --- /dev/null +++ b/BTCPayServer/Views/Error/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_LayoutError"; +} diff --git a/BTCPayServer/Views/Shared/_BTCPaySupporters.cshtml b/BTCPayServer/Views/Shared/_BTCPaySupporters.cshtml new file mode 100644 index 000000000..921df3b12 --- /dev/null +++ b/BTCPayServer/Views/Shared/_BTCPaySupporters.cshtml @@ -0,0 +1,41 @@ +

BTCPayServer Supporters

+ +
+ + Sponsor DG lab + +
+ DG Lab +
+
+
+ + Sponsor ACINQ + +
+ ACINQ +
+
+
+ + Sponsor LunaNode + +
+ LunaNode +
+
+ diff --git a/BTCPayServer/Views/Shared/_LayoutWelcome.cshtml b/BTCPayServer/Views/Shared/_LayoutWelcome.cshtml index c04520c28..0429dc625 100644 --- a/BTCPayServer/Views/Shared/_LayoutWelcome.cshtml +++ b/BTCPayServer/Views/Shared/_LayoutWelcome.cshtml @@ -102,47 +102,7 @@

-

Our supporters

- -
- - Sponsor DG lab - -
- DG Lab -
-
-
- - Sponsor ACINQ - -
- ACINQ -
-
-
- - Sponsor LunaNode - -
- LunaNode -
-
- + @await Html.PartialAsync("_BTCPaySupporters")
@RenderBody() From ef503fa90775a8f05dba89789ef871c27933b3b4 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sat, 1 Feb 2020 01:41:27 -0600 Subject: [PATCH 174/810] Special page to handle 404 errors --- BTCPayServer/Controllers/ErrorController.cs | 8 ++++++++ BTCPayServer/Views/Error/404.cshtml | 15 +++++++++++++++ .../wwwroot/img/errorpages/404_nicolas.jpg | Bin 0 -> 14226 bytes 3 files changed, 23 insertions(+) create mode 100644 BTCPayServer/Views/Error/404.cshtml create mode 100644 BTCPayServer/wwwroot/img/errorpages/404_nicolas.jpg diff --git a/BTCPayServer/Controllers/ErrorController.cs b/BTCPayServer/Controllers/ErrorController.cs index 29a02b9e7..1113aa6da 100644 --- a/BTCPayServer/Controllers/ErrorController.cs +++ b/BTCPayServer/Controllers/ErrorController.cs @@ -12,6 +12,14 @@ namespace BTCPayServer.Controllers { public IActionResult Handle(int? statusCode = null) { + if (statusCode.HasValue) + { + if (statusCode.Value == 404) + { + var viewName = statusCode.ToString(); + return View(viewName); + } + } return View(statusCode); } } diff --git a/BTCPayServer/Views/Error/404.cshtml b/BTCPayServer/Views/Error/404.cshtml new file mode 100644 index 000000000..845f9eaae --- /dev/null +++ b/BTCPayServer/Views/Error/404.cshtml @@ -0,0 +1,15 @@ +@{ + ViewData["ErrorTitle"] = "404 - Page not found"; +} + + diff --git a/BTCPayServer/wwwroot/img/errorpages/404_nicolas.jpg b/BTCPayServer/wwwroot/img/errorpages/404_nicolas.jpg new file mode 100644 index 0000000000000000000000000000000000000000..20f741998f318ad240417e4251486056e0a8f676 GIT binary patch literal 14226 zcmb8VWl&ws@;$ua;O_1&!8O6%-Q9z`ySo$I0t9z=4Hn$p2@u>R_@C#w_x`HhkMEqS z{o%}>+0)b2z1HfP_vQC(07Y6{N*n+K0|2m(AMm~ghyaiv2nYxe#DGd*4?91P~YijsimYu=5T#at=TPU;&6i03fA7`|nH)ks$*C|M^pgYKdcf z!WWG<>A=hDKqJj=a?=&xT3t}~pI5rWxq>DQCT!vv#(w9kWwfUy$Rr_6tC+y>zi!=B|u=ygg^GHV9s;A5wZy zk8xk7WO_J+?*KqBD3Ccl0WATB1K*EK-@dv!<_qn}aCt*kTI_eZFLINN1_JV;Yi(?5 zzdEBBmpG#|j+-3X7$1m;cs_G$|6Xz5WILw2waBej1ORwrYZ9~sh@ae(b4h8jN?5gr zU7U8*Or8@i8J0`?J7zbo`W?o%HJf#b7G28r(kCr?6>d~#72}W4-5zS@Y_pv`)5bij zhczuB006$6`3sGLB*B@GKT{vlgdCgjXmofa(j|LIC%21VAJOL(pMJeU_Zh$QZP2z$ z-A+zTyj>^CXHBDrtMc!Z(3JY}>jUlyu0dWkbxkb#hqLUAzvg8Cup})GK97aB0{l}} zP7J2A_ul+WwZ%Ee`0LiJbCI^SUsy_fxjy8`T^xeI-FIua2AMIY&nrCNU z9vc2`HJ?qmxHt#GqND>YENqIh0T2nGfa|He7j(YLJvootZ8x75d-Z<3Hs`Zp{UdVi z6Xtfji!T%R{9m=tUGZa6mmTz=M$;8ImTqpofA^-&eiFMbb#V@eg!!0{Vji6YMiTIZ zuDPkd-;%+VvLJi){47U9*<* z*R9#8Ix=mRe41744${TMTLEw?;J=2El7I^lSs&@XbP~_Eeiz(7$x>Szvrhba;ANzg zSdzZBcm3FIJ8$F3&>;T}nVig{;iRBDP72QyfJp?v*9Rs96B0rS1S#OGsHd1*R(W0L zCMKA;)Mw9I7*m(^7r&~A6HRtZa?WYZTFb?b#C|Tgi+;WdE&nh<5bJ=-@M9;aD8N`q zsMtHY)ZOVzO>1xLO|U=HKf`3p<@!BH8=sZORV&Uh zqcVS_0dz=kVWyUieq9)2ln#L|-@f})_4T+H6o58o8YQF_tV@F*UL|( zL+wprd)*f+x~1&&%n8-v7UR%{HVkxUqlzvoU&zL5d#(LbgeQK>McoFg;)tqqG9_kd zV>()jNGVu!NmNoaIVCo9=FA25@j=(M8dfRKM60~L)FT@VU|jD?kioEd{nNKqJ*f|A4V z11n%YAOs8&{2f^Qyh^rrgtxHxC2}+1a6pJ;E;4r0a zxjy4T%1MUYOQDx)(ouz%&+?c_wO5`+jceOh#t^ni9! z7vVz=P7?HdWpxPM{8pC<%Wc0PdcMy`0>V5tmG3}F3HdK#>R-FNePg~>9iSX-lo_14 zaR~4kpt&k?`j~Z|Ygp-cs+nv`{Y%aI`FOF9{2!}b!(zk-;Qwc+=wM@~l_(qFOX zs7$Znkg2bEC_)gttBMZtIaPXJ`8)#rf2kbsiYgo34dlmhKYiU}6&!e3Z)8(z#-F`6+_iSV_%%9fY-;%dkjgqxl{E%A^qfN^YqNf_U7rMf_F>6$k!ieM0mMW5Bex@Ri;d zs#J4vL9Jz|VDHs}Fn<;LPP9e}G^nemro}hG9H^f#()OI)id7VqA2B8!1!OMZsQD=v z(hLO5fDOrI#VDdWxD%0Aj;?@ER}}uRi`D=BYaz&h6>a6TQ4mwA@a0MB8cfSXrXXI1 zy!DDtztdyllL$K^vY zeteS4cComaW2{n)Yw*OZ>15Fy(4v%9e5WVlVmB9d%^?hvTRdB7QK~1($#PxZ5O2d>iAM(`7y;XLoHOf$fw0hF3xQ2 zVHnFdXb;~`IbB#trADd}YW|DB1wE!j$tE2aTJ}{XFzp8$PTxyMtyu zc-@d-!QFHzL96VUX729&uVG;Y1s74B8c`7FNkDENfpD$d~f_r;@B54Qq>(z@Tf28!)Eq@W> ziKxI|=Xu;asvA~Sf*n85d|XzH4%CWQnu`r)^}-mQMBhvu^TQF1e$~5o}q1SeyxL zWGP9*vP_tbLIf`Tvrn#k71USOdsfz+dZ?U;%@rJpd324dmQQC>u|GAF_9VHQMt>CUE;2P?thmZCZ~*f#q(t0T85bC&ybJx$A-VP;rwb`woAXT z5i>g9y*2f1Qni#-i-${JVJ`lcX5p!ydQ&5BuNY`tMifQlFzfydk(F^TR^pqh<b6TR`zFE~AB?+$G0dQv#LBtHDQ(}pz0a31}w@4t96HNqHgZ6Eh+yU&)~O|JZD<5yp7 z-ht8|`^D4VSAPn~N4`^&`SN4@EX;HUua~I&7XwF^c>b|Z9TXKN6n0#j;ru5B-W38Z?QJaAddOtx-EGvO z=1pi`ChK8`GS1i(KTKk6IN~71O zezz7Ctw0rqm8FuB{$QoIKKg9n4y6Gt-X)A$9%SfYRcN#}4<&E+%fvp9q>!4DX(k)!y$UFcgmu!i7xn$v)nK-HvPC9+fn~yQ@)5= zuUKMc_uaIWILrV>I%IF8tmxNT-3o!UZsJaQSNF(k?6=E0xj5S=#UB={GwepERtxCc z@-w>0d@S|1QPt}S>)f_84Kt!KQWA&ZetHz*8U^*<5$*?2YJQ`P9lnwtm6;iNV;#ogW!iSM5JSb#xg12yj*OR0 z?=~rqU*5lLFT>ZEE(k67RL{!VZ`1hgTziJX07~Daog(YG6Rfu^eG#Ez<`}jdKIapliPx+KG1vtUYq_&XMrUd9;6ohur$&h!I z|GBB0-Yu}{5E))AJyf@_<03C_Qre$z5bc`G@wTAaTgqr6sq@DO{)O8`Z(zimydAU~WTf6r6#1E2f?m$&hSBRz%ZxR>3_S zTqIAv**}p86!XO&+O&+IlZj>?y%WCCaeCu6l}$U=I7(|jmVb7Op_Rgda&x9zus zhjduA)3{fAEd8A4#LKglD|ukxl^qmH4OD$+__mM)d6MpK*~)lX`OPahVkel@S|dgzMrp*&?T**YzA`&UJ;JE$V|X-9UK%48@}+ z)W`%?mu;#LVIVW&l|351-eV{jc&dwpl$7`~Z)ctsbK5EMM~Y!+B;niBVV>*NR`bEC zi=>=WItP3KZz&ba49Yx{KsX>EAXP2i@s`ZV<;vgA`dzOQfTT&=W*m~wBUNgrp=e{Z zkHBuG=@Aqy4k2wRYZn~e$aie`I-^)K;(3;`TF<|ajs#dKk;b&j(~V?IS1M9U*ZN;^ z@>}nj=GbU=pVSb+jb&T0kvYOe-&x8*t8#m(y#vB{+nqBDSTQ-%Oxs5iR6PqmqDI_`Ic9)j(J!aNth27e{U%F2pLoNM$#`+}6?kyzU4QC7H?(>e1@ zsut#-zG9gO&6txmG3L6>B`K_EZKY-Bm>I{7vuT--yJ|KuE^O8wI*~O^_^Z+jf_6}r zMEF9ZbSe&Ma_T%}OnHA*DE}BfP%j_eVa)Y+_KV(EaBmav5KHopZm!S{ z-IVU@9?Z=sF_lA%t||A-Ci4Yd?)$~ZZ16&7#Co4(13$R={CfFuY%@U%K~%Tp-??PQ zw}QPFkl&L&ahIXOKhZZEG&D>%)evo@Z|09;RbcWkikSLFXB8MZ9fK9?PIsj}qxfJ--eMR!9Y zbV;35CQ(O~jp|ZOg48dJDA-O;ml;v z%kXUNJ2+q|g#c#N* zNx#%Cm^GGP@^kUa=5fwNF>huTSV76BQ;x}uKFLEQUT=`#dlzQ;p{UTa$u-C z#XHb*uRj^evYV%dKrEwHzMy}zX{N1d zs9JsDvCd8mV+5CCGiLu+N7dr2=ObU!qkM>Xa4>L42=Mblk|wnP8^n)A;2sGbeDW+oMp)(@kjaU|gCt%@y%^BS95WzHmZ%y8D+6skqu**dMmp{qc+& z_|*2p>~Ezb0@;hWiDV570(kgN-rUVoVkLd8TY&-zvOqNdVY>l zv8u~CQMif~EG9lR+kO4*fah!BPKHDRHj64kgYc9ar0i=EhD7-Xk8B|xy@1SwP9JLs zdO*90t1hYdd~+71r61|OllIBL(pZ8Evb}A_I>=G6jU@mrz zqyGR7p^tB|dmcL3>GdHDEY`f6zb&fA#N~E!iv~xu9w7|K$xydx1_JX8f+gKWr2&pE ze@2WthUC+HF$W^Yz>7?@b>`eA&4zrWKKbRKpXRC){oQjb&>_*hQ~M5>Lgu{#DDh8` z4#n6ztO6E-C?xa9;LGy38|{EpjQvJi^FmA*T01OHE3P!TUz(aJQ?}5nG~!h zBP-+ImX70r(d;=xLHBl2*%(`I^MeQsKPA`x9IeND6xah==~oEj={F=+i8C(EuQ&-* zJA7BXs|ns-WR@4#-pJX&-ZCg@z}kWv7%1`S)oh zVlQLgR$WAV&6t#;NF5+kGE{eLG17h^b}L9^EbSJ;v~C9^ZR(Sa+~}NztKn~^v)N`O zSNY>y;H4S-W&;^gcga$gp4Ka4(mt0C;!GSM45?`y$R5%~M$bw4J4%3k(!dSQG zeiS}SIGm9+ai{RjVZuro;pk}aeN_!UP%EvyOkfxMsRu`NWYB$8aLy7W12F)L=-wHq~}$tqn8+wYacA8?5vZ_V&MZ#On_4~LI zXxt#Q=w^L|wwcrE z*nmxw^~1akS+`!VukQfbri}ZdD&E{96%X#`OhjHvsfn8bc8!6idi6>2MJ~}SAdOjn z>ev+al{=2GD-e&cLb&Y*#DIlozeNf?jM56*eQI%uQU4X|TZ@GA&8L+V%h)jb#{BYr z%r@LQxT;P^>RpV0&{7>pQn`7#qEBhAo_`p^x>vGJ@4Ygf*Djz*y0pku`ijh|#Tr;- z^vIc)8S^sOK%&0%ywc2So@Rf1555u9YWYi)VT)}nXt~Y zc{#H2;_=ALV2;~pT$K(D)#20o_rC%OuFrol=XP}yT4qtXX0fJ9Wtzm&gTUN_esJ&a zjeHK5(Vf~?LVxR~KJq#peaI1<2bIBfe3KS3T& zL2EV{*Hesw771r`2u9r-AHd_NAho?#+p>WbZau|ql5c=^*2qBl#ng*dbMvE>RPk12S*wPR%e zY!Py=<`}PFP4fKi>x?7F+zG5<*~vpYMxltUkTZmc6d92kck9P|?z9=QmTKS7cjm0c z6!UK@aI_HqQJMaI_?g-jTH~!y11GsRPhpoALYJs)pvL1&vT?3$8Ei`f;cj#7 z+wb6Ma6Yit$(ZsE@fR;AI@wZT^F5mqU;w$}3!Ounem(oE;RCg)c7zYU1-zZDn>O=T){PrXoBJ|J(^@)y~rl^Y#h z;#>MLQ1hDDbDlT&5@~|<%N~WL8=q3VQv`z}d~{sln|u!Cn$t`sS~;4?Ea6jD{N*ZY z|94|z|J^lzC7qGWp6Vh6ts{Z-%Bbv^jCa7m!qfHlHOe~>OdiRII@N?=KkQX9B3`Xm=bvH*FLel!{Ed8(zLBL-br&g z!F5Y09h5Qs4lD~^EcshH;?=Yj<%U}D-*h4S7j~+)*55%Pvb+=wAy)*>9qLm}KX<`g znE1TauypZ@J=q51p1^qA{7ygVFL!Q}`GSsc)qm)v^TtZ%>R|SF>#suPCF!;=$U6`< zkd8U6Qaf%mbNwACA`Ks`TV#SS_27q|mVP46%l62n_&sRS%fflNk`|VF8|!rQeK`I@ zy8e4G4uC#1FG`iB*q#O{q`?&Qirzc0roiiHiJ71qV&GHy{QO|x|8bfE_}=OZv4VNT z225Imoj?#cKCG%iY>HLAERI(Qn@9=*7`EXF9?*&Q9CmDk=NSV40HI{5jQw}+L5EOk-vNn;Qk5y7NYFRLI17VPGd5(D z7hs-+^TA$roa(Jui4I!d=mmp>2v_vS)CS>Z@$TTMuLHm}xDWKDUYll|J?id;AONO7 zQ`GA(Htm6f<5zTC^H|5WHup6*Ji zL!{h^X1V(pxJyN}ZL>Qm?om{PW6a5`q-RYL=qWVO1N|Nbc60oV`8^ z7RsdJ20MU97BETju%XY%44{NqNZrrBJs(uMB`Z+jCFrb!X*#w7r zdJqBLAMEZ7%m>d8s?8?A?~h+5B}=Z->phgTqX(Bt}eg zKt5FY6LRx=|1byqSG}pmYU?yy^$hX+XC)A;5e|MB+N$Ukj-S6iV3;y%Jg`lnYQ{Vu z3rXH5VE%Ub;a^JBkH`Z55GoKK*H8Y_z&>QkNBz}Ei9|55E--&)=l^PYD8A15_cm4; zLR{F8oi(*mUC7>49D$i|M8UtJED|3$lPZIH!k^y|tt>=IslpV%g&-`f+*ecLKN6>r*vdvV~r7Mymv@xqz;5SH?GF5b)jh1X8ZV^!yroYAr2ps$;xUPc1l^)^l%C<6W8HtHom6u zomiA^saP6)qa73MNBmI8%0Ad&WsHY98{KQv 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 Date: Sat, 1 Feb 2020 01:44:01 -0600 Subject: [PATCH 175/810] Special page to handle 500 errors --- BTCPayServer/Controllers/ErrorController.cs | 3 ++- BTCPayServer/Views/Error/500.cshtml | 15 +++++++++++++++ .../wwwroot/img/errorpages/500_mrkukks.jpg | Bin 0 -> 18781 bytes 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 BTCPayServer/Views/Error/500.cshtml create mode 100644 BTCPayServer/wwwroot/img/errorpages/500_mrkukks.jpg diff --git a/BTCPayServer/Controllers/ErrorController.cs b/BTCPayServer/Controllers/ErrorController.cs index 1113aa6da..2bca043f9 100644 --- a/BTCPayServer/Controllers/ErrorController.cs +++ b/BTCPayServer/Controllers/ErrorController.cs @@ -14,7 +14,8 @@ namespace BTCPayServer.Controllers { if (statusCode.HasValue) { - if (statusCode.Value == 404) + var specialPages = new[] { 404, 500 }; + if (specialPages.Any(a => a == statusCode.Value)) { var viewName = statusCode.ToString(); return View(viewName); diff --git a/BTCPayServer/Views/Error/500.cshtml b/BTCPayServer/Views/Error/500.cshtml new file mode 100644 index 000000000..bf014b3f7 --- /dev/null +++ b/BTCPayServer/Views/Error/500.cshtml @@ -0,0 +1,15 @@ +@{ + ViewData["ErrorTitle"] = "500 - Internal Server Error"; +} + + diff --git a/BTCPayServer/wwwroot/img/errorpages/500_mrkukks.jpg b/BTCPayServer/wwwroot/img/errorpages/500_mrkukks.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4fd4ef216053563fc24e32adf367e8d619818c51 GIT binary patch literal 18781 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 Date: Sat, 1 Feb 2020 01:58:34 -0600 Subject: [PATCH 176/810] Special page to handle 429 errors --- BTCPayServer/Controllers/ErrorController.cs | 2 +- BTCPayServer/Views/Error/429.cshtml | 15 +++++++++++++++ BTCPayServer/Views/Error/Handle.cshtml | 2 +- .../wwwroot/img/errorpages/429_rockstardev.jpg | Bin 0 -> 15748 bytes 4 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 BTCPayServer/Views/Error/429.cshtml create mode 100644 BTCPayServer/wwwroot/img/errorpages/429_rockstardev.jpg diff --git a/BTCPayServer/Controllers/ErrorController.cs b/BTCPayServer/Controllers/ErrorController.cs index 2bca043f9..0cafdb4e5 100644 --- a/BTCPayServer/Controllers/ErrorController.cs +++ b/BTCPayServer/Controllers/ErrorController.cs @@ -14,7 +14,7 @@ namespace BTCPayServer.Controllers { if (statusCode.HasValue) { - var specialPages = new[] { 404, 500 }; + var specialPages = new[] { 404, 429, 500 }; if (specialPages.Any(a => a == statusCode.Value)) { var viewName = statusCode.ToString(); diff --git a/BTCPayServer/Views/Error/429.cshtml b/BTCPayServer/Views/Error/429.cshtml new file mode 100644 index 000000000..9ef961fef --- /dev/null +++ b/BTCPayServer/Views/Error/429.cshtml @@ -0,0 +1,15 @@ +@{ + ViewData["ErrorTitle"] = "429 - Too Many Requests"; +} + + diff --git a/BTCPayServer/Views/Error/Handle.cshtml b/BTCPayServer/Views/Error/Handle.cshtml index f14d7711a..12f058f32 100644 --- a/BTCPayServer/Views/Error/Handle.cshtml +++ b/BTCPayServer/Views/Error/Handle.cshtml @@ -15,5 +15,5 @@ Consult server log for more details.

Navigate back to home. -


+

diff --git a/BTCPayServer/wwwroot/img/errorpages/429_rockstardev.jpg b/BTCPayServer/wwwroot/img/errorpages/429_rockstardev.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ef75fd50f62f3fc10e60422208e3e25298f2ce0a GIT binary patch literal 15748 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 x#G}ak$KLJO) literal 0 HcmV?d00001 From ea9169f607622003e9215927b50ac2f9d7d8db57 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sat, 1 Feb 2020 02:29:08 -0600 Subject: [PATCH 177/810] Updating 404 page not found assert --- BTCPayServer.Tests/SeleniumTester.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index c7f163312..e97b958d5 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -202,7 +202,7 @@ namespace BTCPayServer.Tests internal void AssertNotFound() { - Assert.Contains("Status Code: 404; Not Found", Driver.PageSource); + Assert.Contains("404 - Page not found", Driver.PageSource); } public void GoToHome() From b432d8903f5c698c4d3cb7d6c17e4d99f3352cbc Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sat, 1 Feb 2020 11:16:40 -0600 Subject: [PATCH 178/810] Grammar fix by Kukks --- BTCPayServer/Views/Error/404.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/Views/Error/404.cshtml b/BTCPayServer/Views/Error/404.cshtml index 845f9eaae..10daba9ff 100644 --- a/BTCPayServer/Views/Error/404.cshtml +++ b/BTCPayServer/Views/Error/404.cshtml @@ -3,7 +3,7 @@ }

diff --git a/BTCPayServer/wwwroot/js/WalletSend.js b/BTCPayServer/wwwroot/js/WalletSend.js index dcfd49f89..7976ae3b7 100644 --- a/BTCPayServer/wwwroot/js/WalletSend.js +++ b/BTCPayServer/wwwroot/js/WalletSend.js @@ -44,4 +44,12 @@ $(function () { updateFiatValue(outputAmountElement); return false; }); + + $("#bip21parse").on("click", function(){ + var bip21 = prompt("Paste BIP21 here"); + if(bip21){ + $("#BIP21").val(bip21); + $("form").submit(); + } + }); }); From db6a4687d25f649bdca03aceb95f4f40051dc5ea Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Thu, 13 Feb 2020 14:06:00 +0100 Subject: [PATCH 190/810] Wallet prep work for BPU (#1331) * Wallet prep work for BPU This PR prepares the wallet for #1321. It makes transfers from the vault and ledger to go to their own post actions for processing (not particularly useful in this PR but is needed in BPU to propose a new tx) It also makes the Sign with seed consistent with redirect to /psbt/ready after signing which it did not do (it stayed on the seed route) * fix test * add assert --- BTCPayServer.Tests/PSBTTests.cs | 21 ++- BTCPayServer.Tests/SeleniumTests.cs | 3 +- .../Controllers/WalletsController.PSBT.cs | 26 ++-- BTCPayServer/Controllers/WalletsController.cs | 48 +++++-- BTCPayServer/Views/Wallets/WalletPSBT.cshtml | 4 +- .../Views/Wallets/WalletPSBTReady.cshtml | 126 ++++++++++-------- .../Views/Wallets/WalletSendLedger.cshtml | 12 +- .../Views/Wallets/WalletSendVault.cshtml | 12 +- 8 files changed, 157 insertions(+), 95 deletions(-) diff --git a/BTCPayServer.Tests/PSBTTests.cs b/BTCPayServer.Tests/PSBTTests.cs index 1c41890e2..8649bc660 100644 --- a/BTCPayServer.Tests/PSBTTests.cs +++ b/BTCPayServer.Tests/PSBTTests.cs @@ -71,7 +71,7 @@ namespace BTCPayServer.Tests BitcoinAddress.Create(vmLedger.HintChange, user.SupportedNetwork.NBitcoinNetwork); Assert.NotNull(vmLedger.WebsocketPath); - string redirectedPSBT = AssertRedirectedPSBT(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt")); + string redirectedPSBT = AssertRedirectedPSBT(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt"), nameof(walletController.WalletPSBT)); var vmPSBT = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel() { PSBT = redirectedPSBT }).AssertViewModelAsync(); var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork); Assert.NotNull(vmPSBT.Decoded); @@ -80,14 +80,20 @@ namespace BTCPayServer.Tests PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork); await walletController.WalletPSBT(walletId, vmPSBT, "ledger").AssertViewModelAsync(); - var vmPSBT2 = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync(); + var vmPSBT2 = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel() + { + PSBT = AssertRedirectedPSBT( await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady)) + } ).AssertViewModelAsync(); Assert.NotEmpty(vmPSBT2.Inputs.Where(i => i.Error != null)); Assert.Equal(vmPSBT.PSBT, vmPSBT2.PSBT); var signedPSBT = unsignedPSBT.Clone(); signedPSBT.SignAll(user.DerivationScheme, user.ExtKey); vmPSBT.PSBT = signedPSBT.ToBase64(); - var psbtReady = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync(); + var psbtReady = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel() + { + PSBT = AssertRedirectedPSBT( await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady)) + } ).AssertViewModelAsync(); Assert.Equal(2 + 1, psbtReady.Destinations.Count); // The fee is a destination Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive); Assert.Contains(psbtReady.Destinations, d => d.Positive); @@ -98,7 +104,7 @@ namespace BTCPayServer.Tests var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync(); Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT); combineVM.PSBT = signedPSBT.ToBase64(); - var psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM)); + var psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT)); var signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork); Assert.True(signedPSBT.TryFinalize(out _)); @@ -108,7 +114,7 @@ namespace BTCPayServer.Tests // Can use uploaded file? combineVM.PSBT = null; combineVM.UploadedPSBTFile = TestUtils.GetFormFile("signedPSBT", signedPSBT.ToBytes()); - psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM)); + psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT)); signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork); Assert.True(signedPSBT.TryFinalize(out _)); Assert.True(signedPSBT2.TryFinalize(out _)); @@ -116,17 +122,18 @@ namespace BTCPayServer.Tests var ready = (await walletController.WalletPSBTReady(walletId, signedPSBT.ToBase64())).AssertViewModel(); Assert.Equal(signedPSBT.ToBase64(), ready.PSBT); - psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt")); + psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"), nameof(walletController.WalletPSBT)); Assert.Equal(signedPSBT.ToBase64(), psbt); redirect = Assert.IsType(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast")); Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName); } } - private static string AssertRedirectedPSBT(IActionResult view) + private static string AssertRedirectedPSBT(IActionResult view, string actionName) { var postRedirectView = Assert.IsType(view); var postRedirectViewModel = Assert.IsType(postRedirectView.Model); + Assert.Equal(actionName, postRedirectViewModel.AspAction); var redirectedPSBT = postRedirectViewModel.Parameters.Single(p => p.Key == "psbt").Value; return redirectedPSBT; } diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index ff01ad6b9..5b072bdca 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -566,9 +566,10 @@ namespace BTCPayServer.Tests Assert.Contains(jack.ToString(), s.Driver.PageSource); Assert.Contains("0.01000000", s.Driver.PageSource); s.Driver.FindElement(By.CssSelector("button[value=analyze-psbt]")).ForceClick(); + Assert.EndsWith("psbt", s.Driver.Url); s.Driver.FindElement(By.CssSelector("#OtherActions")).ForceClick(); s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick(); - Assert.EndsWith("psbt", s.Driver.Url); + Assert.EndsWith("psbt/ready", s.Driver.Url); s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick(); Assert.Equal(walletTransactionLink, s.Driver.Url); diff --git a/BTCPayServer/Controllers/WalletsController.PSBT.cs b/BTCPayServer/Controllers/WalletsController.PSBT.cs index 82efa784d..e30d5ec7c 100644 --- a/BTCPayServer/Controllers/WalletsController.PSBT.cs +++ b/BTCPayServer/Controllers/WalletsController.PSBT.cs @@ -20,7 +20,6 @@ namespace BTCPayServer.Controllers { var nbx = ExplorerClientProvider.GetExplorerClient(network); CreatePSBTRequest psbtRequest = new CreatePSBTRequest(); - foreach (var transactionOutput in sendModel.Outputs) { var psbtDestination = new CreatePSBTDestination(); @@ -65,7 +64,7 @@ namespace BTCPayServer.Controllers vm.Decoded = psbt.ToString(); vm.PSBT = psbt.ToBase64(); } - return View(vm ?? new WalletPSBTViewModel() { CryptoCode = walletId.CryptoCode }); + return View(nameof(WalletPSBT), vm ?? new WalletPSBTViewModel() { CryptoCode = walletId.CryptoCode }); } [HttpPost] [Route("{walletId}/psbt")] @@ -107,7 +106,7 @@ namespace BTCPayServer.Controllers return View(vm); } TempData[WellKnownTempData.SuccessMessage] = "PSBT updated!"; - return RedirectToWalletPSBT(walletId, psbt, vm.FileName); + return RedirectToWalletPSBT(psbt, vm.FileName); case "seed": return SignWithSeed(walletId, psbt.ToBase64()); case "nbx-seed": @@ -125,7 +124,7 @@ namespace BTCPayServer.Controllers return View(vm); case "broadcast": { - return await WalletPSBTReady(walletId, psbt.ToBase64()); + return RedirectToWalletPSBTReady(psbt.ToBase64()); } case "combine": ModelState.Remove(nameof(vm.PSBT)); @@ -162,7 +161,6 @@ namespace BTCPayServer.Controllers var vm = new WalletPSBTReadyViewModel() { PSBT = psbt }; vm.SigningKey = signingKey; vm.SigningKeyPath = signingKeyPath; - var derivationSchemeSettings = GetDerivationSchemeSettings(walletId); if (derivationSchemeSettings == null) return NotFound(); @@ -224,7 +222,7 @@ namespace BTCPayServer.Controllers vm.CanCalculateBalance = true; vm.Positive = balanceChange >= Money.Zero; } - + vm.Inputs = new List(); foreach (var input in psbtObject.Inputs) { var inputVm = new WalletPSBTReadyViewModel.InputViewModel(); @@ -237,7 +235,7 @@ namespace BTCPayServer.Controllers inputVm.Positive = balanceChange2 >= Money.Zero; inputVm.Index = (int)input.Index; } - + vm.Destinations = new List(); foreach (var output in psbtObject.Outputs) { var dest = new WalletPSBTReadyViewModel.DestinationViewModel(); @@ -297,14 +295,14 @@ namespace BTCPayServer.Controllers catch { vm.GlobalError = "Invalid PSBT"; - return View(vm); + return View(nameof(WalletPSBTReady),vm); } if (command == "broadcast") { if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors)) { vm.SetErrors(errors); - return View(vm); + return View(nameof(WalletPSBTReady),vm); } var transaction = psbt.ExtractTransaction(); try @@ -313,24 +311,24 @@ namespace BTCPayServer.Controllers if (!broadcastResult.Success) { vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}"; - return View(vm); + return View(nameof(WalletPSBTReady),vm); } } catch (Exception ex) { vm.GlobalError = "Error while broadcasting: " + ex.Message; - return View(vm); + return View(nameof(WalletPSBTReady),vm); } return RedirectToWalletTransaction(walletId, transaction); } else if (command == "analyze-psbt") { - return RedirectToWalletPSBT(walletId, psbt); + return RedirectToWalletPSBT(psbt); } else { vm.GlobalError = "Unknown command"; - return View(vm); + return View(nameof(WalletPSBTReady),vm); } } @@ -359,7 +357,7 @@ namespace BTCPayServer.Controllers } sourcePSBT = sourcePSBT.Combine(psbt); TempData[WellKnownTempData.SuccessMessage] = "PSBT Successfully combined!"; - return RedirectToWalletPSBT(walletId, sourcePSBT); + return RedirectToWalletPSBT(sourcePSBT); } } } diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index a5330c8df..0f2d63d5d 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using System.Net.WebSockets; @@ -8,7 +7,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Data; -using BTCPayServer.Events; using BTCPayServer.HostedServices; using BTCPayServer.ModelBinders; using BTCPayServer.Models; @@ -19,21 +17,15 @@ using BTCPayServer.Services; using BTCPayServer.Services.Rates; using BTCPayServer.Services.Stores; using BTCPayServer.Services.Wallets; -using LedgerWallet; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore.Metadata.Internal; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; using NBitcoin; using NBitcoin.DataEncoders; using NBXplorer; using NBXplorer.DerivationStrategy; using NBXplorer.Models; using Newtonsoft.Json; -using static BTCPayServer.Controllers.StoresController; namespace BTCPayServer.Controllers { @@ -420,7 +412,7 @@ namespace BTCPayServer.Controllers var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.AccountDerivation); model.NBXSeedAvailable = await CanUseHotWallet() && !string.IsNullOrEmpty(await ExplorerClientProvider.GetExplorerClient(network) .GetMetadataAsync(GetDerivationSchemeSettings(walletId).AccountDerivation, - WellknownMetadataKeys.Mnemonic)); + WellknownMetadataKeys.MasterHDKey)); model.CurrentBalance = await balance; model.RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi; model.FeeSatoshiPerByte = model.RecommendedSatoshiPerByte; @@ -593,7 +585,7 @@ namespace BTCPayServer.Controllers case "analyze-psbt": var name = $"Send-{string.Join('_', vm.Outputs.Select(output => $"{output.Amount}->{output.DestinationAddress}{(output.SubtractFeesFromOutput ? "-Fees" : string.Empty)}"))}.psbt"; - return RedirectToWalletPSBT(walletId, psbt.PSBT, name); + return RedirectToWalletPSBT(psbt.PSBT, name); default: return View(vm); } @@ -651,7 +643,31 @@ namespace BTCPayServer.Controllers }); } - private IActionResult RedirectToWalletPSBT(WalletId walletId, PSBT psbt, string fileName = null) + [HttpPost] + [Route("{walletId}/vault")] + public async Task SubmitVault([ModelBinder(typeof(WalletIdModelBinder))] + WalletId walletId, WalletSendVaultModel model) + { + + return RedirectToWalletPSBTReady(model.PSBT); + } + private IActionResult RedirectToWalletPSBTReady(string psbt, string signingKey= null, string signingKeyPath = null) + { + var vm = new PostRedirectViewModel() + { + AspController = "Wallets", + AspAction = nameof(WalletPSBTReady), + Parameters = + { + new KeyValuePair("psbt", psbt), + new KeyValuePair("SigningKey", signingKey), + new KeyValuePair("SigningKeyPath", signingKeyPath) + } + }; + return View("PostRedirect", vm); + } + + private IActionResult RedirectToWalletPSBT(PSBT psbt, string fileName = null) { var vm = new PostRedirectViewModel() { @@ -700,6 +716,14 @@ namespace BTCPayServer.Controllers }); } + [HttpPost] + [Route("{walletId}/ledger")] + public async Task SubmitLedger([ModelBinder(typeof(WalletIdModelBinder))] + WalletId walletId, WalletSendLedgerModel model) + { + return RedirectToWalletPSBTReady(model.PSBT); + } + [HttpGet("{walletId}/psbt/seed")] public IActionResult SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,string psbt) @@ -773,7 +797,7 @@ namespace BTCPayServer.Controllers return View(viewModel); } ModelState.Remove(nameof(viewModel.PSBT)); - return await WalletPSBTReady(walletId, psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath?.ToString()); + return RedirectToWalletPSBTReady(psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath?.ToString()); } private bool PSBTChanged(PSBT psbt, Action act) diff --git a/BTCPayServer/Views/Wallets/WalletPSBT.cshtml b/BTCPayServer/Views/Wallets/WalletPSBT.cshtml index e090eb6af..b678237c9 100644 --- a/BTCPayServer/Views/Wallets/WalletPSBT.cshtml +++ b/BTCPayServer/Views/Wallets/WalletPSBT.cshtml @@ -28,7 +28,7 @@ {

Decoded PSBT

-
+ @@ -65,7 +65,7 @@
@Model.Decoded
}

PSBT to decode

- +
diff --git a/BTCPayServer/Views/Wallets/WalletPSBTReady.cshtml b/BTCPayServer/Views/Wallets/WalletPSBTReady.cshtml index 0072f9cdc..ee491ac41 100644 --- a/BTCPayServer/Views/Wallets/WalletPSBTReady.cshtml +++ b/BTCPayServer/Views/Wallets/WalletPSBTReady.cshtml @@ -2,13 +2,24 @@ @{ Layout = "../Shared/_Layout.cshtml"; } +
+ @if (TempData.HasStatusMessage()) + { +
+
+ +
+
+ } @if (Model.GlobalError != null) { }
@@ -18,14 +29,16 @@ @if (Model.CanCalculateBalance) {

- If you broadcast this transaction, your balance will change: @if (Model.Positive) + If you broadcast this transaction, your balance will change: + @if (Model.Positive) { @Model.BalanceChange } else { @Model.BalanceChange - }, do you want to continue? + } + , do you want to continue?

} else @@ -40,36 +53,38 @@

Inputs

- - - - + + + + - @foreach (var input in Model.Inputs) - { - - @if (input.Error != null) - { - - } - else - { - - } + @foreach (var input in Model.Inputs) + { + + @if (input.Error != null) + { + + } + else + { + + } - @if (input.Positive) - { - - } - else - { - - } - - } + @if (input.Positive) + { + + } + else + { + + } + + }
- Index - Amount
+ Index + Amount
@input.Index @input.Index
+ @input.Index + @input.Index@input.BalanceChange@input.BalanceChange
@input.BalanceChange@input.BalanceChange
@@ -82,28 +97,28 @@

Outputs

- - - - + + + + - @foreach (var destination in Model.Destinations) - { - - - @if (destination.Positive) - { - - } - else - { - - } - - } + @foreach (var destination in Model.Destinations) + { + + + @if (destination.Positive) + { + + } + else + { + + } + + }
- Destination - Amount
+ Destination + Amount
@destination.Destination@destination.Balance@destination.Balance
@destination.Destination@destination.Balance@destination.Balance
@@ -122,12 +137,13 @@
- - - + + + @if (!Model.HasErrors) { - or + + or } diff --git a/BTCPayServer/Views/Wallets/WalletSendLedger.cshtml b/BTCPayServer/Views/Wallets/WalletSendLedger.cshtml index 053809314..0e5e4a8c4 100644 --- a/BTCPayServer/Views/Wallets/WalletSendLedger.cshtml +++ b/BTCPayServer/Views/Wallets/WalletSendLedger.cshtml @@ -6,14 +6,22 @@ }

Sign the transaction with Ledger

+@if (TempData.HasStatusMessage()) +{ +
+
+ +
+
+}
- diff --git a/BTCPayServer/Views/Wallets/WalletSendVault.cshtml b/BTCPayServer/Views/Wallets/WalletSendVault.cshtml index 2ae23445b..fa72b664e 100644 --- a/BTCPayServer/Views/Wallets/WalletSendVault.cshtml +++ b/BTCPayServer/Views/Wallets/WalletSendVault.cshtml @@ -6,15 +6,23 @@ }

Sign the transaction with BTCPayServer Vault

+@if (TempData.HasStatusMessage()) +{ +
+
+ +
+
+}
-
From a2d657f5cb207e8a05f354e80aa83a13fb5cfbfe Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Thu, 13 Feb 2020 23:58:48 +0900 Subject: [PATCH 191/810] Fix mysql migration --- BTCPayServer.Data/Migrations/20190225091644_AddOpenIddict.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/BTCPayServer.Data/Migrations/20190225091644_AddOpenIddict.cs b/BTCPayServer.Data/Migrations/20190225091644_AddOpenIddict.cs index 2e75a3523..9ba5de138 100644 --- a/BTCPayServer.Data/Migrations/20190225091644_AddOpenIddict.cs +++ b/BTCPayServer.Data/Migrations/20190225091644_AddOpenIddict.cs @@ -40,6 +40,11 @@ namespace BTCPayServer.Migrations onDelete: ReferentialAction.Restrict); }); + if (this.IsMySql(migrationBuilder.ActiveProvider)) + { + migrationBuilder.AlterColumn(name: "Id", table: "AspNetUsers", maxLength: maxLength); + } + migrationBuilder.CreateTable( name: "OpenIddictScopes", columns: table => new From ff4c7c364ea890471cc722d3941553fb026641c9 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 14 Feb 2020 00:02:47 +0900 Subject: [PATCH 192/810] Fix mysql --- .../Migrations/20190225091644_AddOpenIddict.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/BTCPayServer.Data/Migrations/20190225091644_AddOpenIddict.cs b/BTCPayServer.Data/Migrations/20190225091644_AddOpenIddict.cs index 9ba5de138..cdc3b848b 100644 --- a/BTCPayServer.Data/Migrations/20190225091644_AddOpenIddict.cs +++ b/BTCPayServer.Data/Migrations/20190225091644_AddOpenIddict.cs @@ -12,6 +12,10 @@ namespace BTCPayServer.Migrations protected override void Up(MigrationBuilder migrationBuilder) { int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null; + if (this.IsMySql(migrationBuilder.ActiveProvider)) + { + migrationBuilder.AlterColumn(name: "Id", table: "AspNetUsers", maxLength: maxLength); + } migrationBuilder.CreateTable( name: "OpenIddictApplications", columns: table => new @@ -40,11 +44,6 @@ namespace BTCPayServer.Migrations onDelete: ReferentialAction.Restrict); }); - if (this.IsMySql(migrationBuilder.ActiveProvider)) - { - migrationBuilder.AlterColumn(name: "Id", table: "AspNetUsers", maxLength: maxLength); - } - migrationBuilder.CreateTable( name: "OpenIddictScopes", columns: table => new From ee9905e85a9f103d1ccf032aeb9c51a027b86ccf Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 14 Feb 2020 00:07:19 +0900 Subject: [PATCH 193/810] Fix mysql --- .../Migrations/20190225091644_AddOpenIddict.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/BTCPayServer.Data/Migrations/20190225091644_AddOpenIddict.cs b/BTCPayServer.Data/Migrations/20190225091644_AddOpenIddict.cs index cdc3b848b..fb6fb2d47 100644 --- a/BTCPayServer.Data/Migrations/20190225091644_AddOpenIddict.cs +++ b/BTCPayServer.Data/Migrations/20190225091644_AddOpenIddict.cs @@ -12,10 +12,6 @@ namespace BTCPayServer.Migrations protected override void Up(MigrationBuilder migrationBuilder) { int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null; - if (this.IsMySql(migrationBuilder.ActiveProvider)) - { - migrationBuilder.AlterColumn(name: "Id", table: "AspNetUsers", maxLength: maxLength); - } migrationBuilder.CreateTable( name: "OpenIddictApplications", columns: table => new @@ -31,7 +27,8 @@ namespace BTCPayServer.Migrations Properties = table.Column(nullable: true), RedirectUris = table.Column(nullable: true), Type = table.Column(maxLength: 25, nullable: false), - ApplicationUserId = table.Column(nullable: true, maxLength: maxLength) + // do not set the maxLength, else http://MySql.Data.MySqlClient.MySqlException (0x80004005): Can't create table `btcpay`.`OpenIddictApplications` (errno: 150 "Foreign key constraint is incorrectly formed") + ApplicationUserId = table.Column(nullable: true) }, constraints: table => { From 5d817a0483d3371a6bad6b9a8ad8e3c2ac444220 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 14 Feb 2020 00:23:00 +0900 Subject: [PATCH 194/810] Revert fix mysql --- BTCPayServer.Data/Migrations/20190225091644_AddOpenIddict.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/BTCPayServer.Data/Migrations/20190225091644_AddOpenIddict.cs b/BTCPayServer.Data/Migrations/20190225091644_AddOpenIddict.cs index fb6fb2d47..2e75a3523 100644 --- a/BTCPayServer.Data/Migrations/20190225091644_AddOpenIddict.cs +++ b/BTCPayServer.Data/Migrations/20190225091644_AddOpenIddict.cs @@ -27,8 +27,7 @@ namespace BTCPayServer.Migrations Properties = table.Column(nullable: true), RedirectUris = table.Column(nullable: true), Type = table.Column(maxLength: 25, nullable: false), - // do not set the maxLength, else http://MySql.Data.MySqlClient.MySqlException (0x80004005): Can't create table `btcpay`.`OpenIddictApplications` (errno: 150 "Foreign key constraint is incorrectly formed") - ApplicationUserId = table.Column(nullable: true) + ApplicationUserId = table.Column(nullable: true, maxLength: maxLength) }, constraints: table => { From 6015eb337a9bb5aabfae9f19dcc507d991b794e5 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sat, 15 Feb 2020 14:36:36 +0900 Subject: [PATCH 195/810] Fix broken link --- BTCPayServer/Views/Server/DynamicDnsServices.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/Views/Server/DynamicDnsServices.cshtml b/BTCPayServer/Views/Server/DynamicDnsServices.cshtml index d5af68b68..69fec4471 100644 --- a/BTCPayServer/Views/Server/DynamicDnsServices.cshtml +++ b/BTCPayServer/Views/Server/DynamicDnsServices.cshtml @@ -16,7 +16,7 @@ This is recommended if you are hosting BTCPayServer at home and wish to have a clearnet HTTPS address to access your server.

-

Note that you need to properly configure your NAT and BTCPayServer install to get HTTPS certificate. Check the documentation for more information.

+

Note that you need to properly configure your NAT and BTCPayServer install to get HTTPS certificate. Check the documentation for more information.

From 821b90416336bde728d08cabb13000e37310ad05 Mon Sep 17 00:00:00 2001 From: Charles Hill Date: Sat, 15 Feb 2020 06:37:29 +0100 Subject: [PATCH 196/810] Added SendGrid, Mailgun to Quick-fill email settings (#1335) --- BTCPayServer/Views/Shared/EmailsBody.cshtml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BTCPayServer/Views/Shared/EmailsBody.cshtml b/BTCPayServer/Views/Shared/EmailsBody.cshtml index 92c4adbfa..ae3c412cc 100644 --- a/BTCPayServer/Views/Shared/EmailsBody.cshtml +++ b/BTCPayServer/Views/Shared/EmailsBody.cshtml @@ -18,7 +18,9 @@

From 143c9098126a9d45609bfb353b152ada36dcb72e Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sun, 16 Feb 2020 19:58:53 +0900 Subject: [PATCH 197/810] bump --- Build/Version.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Build/Version.csproj b/Build/Version.csproj index 6fb28e1b7..da4be94b0 100644 --- a/Build/Version.csproj +++ b/Build/Version.csproj @@ -1,5 +1,5 @@ - 1.0.3.156 + 1.0.3.157 From c03dc48fe95f9840acf9b45df6d9630f9bf35bf0 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sun, 16 Feb 2020 22:07:35 +0900 Subject: [PATCH 198/810] Do not crash if can't load rate cache --- .../HostedServices/RatesHostedService.cs | 20 +++++++++++-------- Build/Version.csproj | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/BTCPayServer/HostedServices/RatesHostedService.cs b/BTCPayServer/HostedServices/RatesHostedService.cs index cf6fb1b62..7c0002f23 100644 --- a/BTCPayServer/HostedServices/RatesHostedService.cs +++ b/BTCPayServer/HostedServices/RatesHostedService.cs @@ -115,20 +115,24 @@ namespace BTCPayServer.HostedServices private async Task TryLoadRateCache() { - var cache = await _SettingsRepository.GetSettingAsync(); - if (cache != null) + try { - _LastCacheDate = cache.Created; - var stateByExchange = cache.States.ToDictionary(o => o.ExchangeName); - foreach (var provider in _RateProviderFactory.Providers) + var cache = await _SettingsRepository.GetSettingAsync(); + if (cache != null) { - if (stateByExchange.TryGetValue(provider.Key, out var state) && - provider.Value is BackgroundFetcherRateProvider fetcher) + _LastCacheDate = cache.Created; + var stateByExchange = cache.States.ToDictionary(o => o.ExchangeName); + foreach (var provider in _RateProviderFactory.Providers) { - fetcher.LoadState(state); + if (stateByExchange.TryGetValue(provider.Key, out var state) && + provider.Value is BackgroundFetcherRateProvider fetcher) + { + fetcher.LoadState(state); + } } } } + catch { } } DateTimeOffset? _LastCacheDate; diff --git a/Build/Version.csproj b/Build/Version.csproj index da4be94b0..f5a4f7215 100644 --- a/Build/Version.csproj +++ b/Build/Version.csproj @@ -1,5 +1,5 @@ - 1.0.3.157 + 1.0.3.158 From f2aab4cf039793c0d3b7e84796d29a8a6944e101 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sun, 16 Feb 2020 23:04:48 +0900 Subject: [PATCH 199/810] Add warning if fail to load rates from cache --- BTCPayServer.Tests/RateRulesTest.cs | 30 +++++++++++++++++++ .../HostedServices/RatesHostedService.cs | 5 +++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/BTCPayServer.Tests/RateRulesTest.cs b/BTCPayServer.Tests/RateRulesTest.cs index 66fd9bade..c989fa658 100644 --- a/BTCPayServer.Tests/RateRulesTest.cs +++ b/BTCPayServer.Tests/RateRulesTest.cs @@ -5,6 +5,7 @@ using System.Text; using BTCPayServer.Rating; using Xunit; using System.Globalization; +using Newtonsoft.Json; namespace BTCPayServer.Tests { @@ -24,6 +25,35 @@ namespace BTCPayServer.Tests Assert.Equal(1.1m, rule.BidAsk.Ask); } + [Fact] + [Trait("Fast", "Fast")] + public void CanSerializeExchangeRatesCache() + { + HostedServices.RatesHostedService.ExchangeRatesCache cache = new HostedServices.RatesHostedService.ExchangeRatesCache(); + cache.Created = DateTimeOffset.UtcNow; + cache.States = new List(); + cache.States.Add(new Services.Rates.BackgroundFetcherState() + { + ExchangeName = "Kraken", + LastRequested = DateTimeOffset.UtcNow, + LastUpdated = DateTimeOffset.UtcNow, + Rates = new List() + { + new Services.Rates.BackgroundFetcherRate() + { + Pair = new CurrencyPair("USD", "BTC"), + BidAsk = new BidAsk(1.0m, 2.0m) + } + } + }); + var str = JsonConvert.SerializeObject(cache, Formatting.Indented); + + var cache2 = JsonConvert.DeserializeObject(str); + Assert.Equal(cache.Created.ToUnixTimeSeconds(), cache2.Created.ToUnixTimeSeconds()); + Assert.Equal(cache.States[0].Rates[0].BidAsk, cache2.States[0].Rates[0].BidAsk); + Assert.Equal(cache.States[0].Rates[0].Pair, cache2.States[0].Rates[0].Pair); + } + [Fact] [Trait("Fast", "Fast")] public void CanParseRateRules() diff --git a/BTCPayServer/HostedServices/RatesHostedService.cs b/BTCPayServer/HostedServices/RatesHostedService.cs index 7c0002f23..f3a34a154 100644 --- a/BTCPayServer/HostedServices/RatesHostedService.cs +++ b/BTCPayServer/HostedServices/RatesHostedService.cs @@ -132,7 +132,10 @@ namespace BTCPayServer.HostedServices } } } - catch { } + catch (Exception ex) + { + Logs.PayServer.LogWarning(ex, "Warning: Error while trying to load rates from cache"); + } } DateTimeOffset? _LastCacheDate; From d31bff7070b522fad6577d475c790f222bbfd676 Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Wed, 19 Feb 2020 09:35:23 +0100 Subject: [PATCH 200/810] BPU Prep Work Part2 (#1340) * BPU Prep Work Part2 * Adjust tests to use the hot wallet when registering deriv scheme * Add amount to payment data view for onchain payments * Make zone limits higher when in dev mode (for tests in next PR) * Make IPaymentMethodDetails serialize/deserialize through payment type using the network * Allow named settings through settings repo * Refactor some extensions for next PR * pr changes * use json convert for now --- BTCPayServer.Tests/PSBTTests.cs | 2 +- BTCPayServer.Tests/TestAccount.cs | 42 +++++++++++++------ BTCPayServer.Tests/UnitTest1.cs | 6 ++- .../Configuration/ExternalConnectionString.cs | 4 +- BTCPayServer/Extensions.cs | 20 +++++++-- BTCPayServer/Hosting/BTCPayServerServices.cs | 18 ++++++-- .../InvoicingModels/InvoiceDetailsModel.cs | 2 + BTCPayServer/Payments/PaymentTypes.Bitcoin.cs | 9 +++- .../Payments/PaymentTypes.Lightning.cs | 7 +++- BTCPayServer/Payments/PaymentTypes.cs | 3 +- .../Monero/Payments/MoneroPaymentType.cs | 7 +++- .../Services/Invoices/InvoiceEntity.cs | 5 +-- BTCPayServer/Services/SettingsRepository.cs | 8 ++-- .../Shared/ViewBitcoinLikePaymentData.cshtml | 3 ++ BTCPayServer/wwwroot/js/WalletSend.js | 2 - 15 files changed, 98 insertions(+), 40 deletions(-) diff --git a/BTCPayServer.Tests/PSBTTests.cs b/BTCPayServer.Tests/PSBTTests.cs index 8649bc660..85130c43e 100644 --- a/BTCPayServer.Tests/PSBTTests.cs +++ b/BTCPayServer.Tests/PSBTTests.cs @@ -88,7 +88,7 @@ namespace BTCPayServer.Tests Assert.Equal(vmPSBT.PSBT, vmPSBT2.PSBT); var signedPSBT = unsignedPSBT.Clone(); - signedPSBT.SignAll(user.DerivationScheme, user.ExtKey); + signedPSBT.SignAll(user.DerivationScheme, user.GenerateWalletResponseV.AccountHDKey, user.GenerateWalletResponseV.AccountKeyPath); vmPSBT.PSBT = signedPSBT.ToBase64(); var psbtReady = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel() { diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index 2d5cbf75f..f73e155cc 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -21,6 +21,7 @@ using BTCPayServer.Data; using OpenIddict.Abstractions; using OpenIddict.Core; using Microsoft.AspNetCore.Identity; +using NBXplorer.Models; namespace BTCPayServer.Tests { @@ -43,18 +44,13 @@ namespace BTCPayServer.Tests var userManager = parent.PayTester.GetService>(); var u = await userManager.FindByIdAsync(UserId); await userManager.AddToRoleAsync(u, Roles.ServerAdmin); + IsAdmin = true; } public void Register() { RegisterAsync().GetAwaiter().GetResult(); } - - public BitcoinExtKey ExtKey - { - get; set; - } - public async Task GrantAccessAsync() { await RegisterAsync(); @@ -100,26 +96,46 @@ namespace BTCPayServer.Tests public BTCPayNetwork SupportedNetwork { get; set; } - public WalletId RegisterDerivationScheme(string crytoCode, bool segwit = false) + public WalletId RegisterDerivationScheme(string crytoCode, bool segwit = false, bool importKeysToNBX = false) { - return RegisterDerivationSchemeAsync(crytoCode, segwit).GetAwaiter().GetResult(); + return RegisterDerivationSchemeAsync(crytoCode, segwit, importKeysToNBX).GetAwaiter().GetResult(); } - public async Task RegisterDerivationSchemeAsync(string cryptoCode, bool segwit = false) + public async Task RegisterDerivationSchemeAsync(string cryptoCode, bool segwit = false, bool importKeysToNBX = false) { SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode); var store = parent.PayTester.GetController(UserId, StoreId); - ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork); - DerivationScheme = SupportedNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(ExtKey.Neuter().ToString() + (segwit ? "" : "-[legacy]")); + GenerateWalletResponseV = await parent.ExplorerClient.GenerateWalletAsync(new GenerateWalletRequest() + { + ScriptPubKeyType = segwit ? ScriptPubKeyType.Segwit : ScriptPubKeyType.Legacy, + SavePrivateKeys = importKeysToNBX + }); + await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel() { + Enabled = true, + CryptoCode = cryptoCode, + Network = SupportedNetwork, + RootFingerprint = GenerateWalletResponseV.AccountKeyPath.MasterFingerprint.ToString(), + RootKeyPath = SupportedNetwork.GetRootKeyPath(), + Source = "NBXplorer", + AccountKey = GenerateWalletResponseV.AccountHDKey.Neuter().ToWif(), + DerivationSchemeFormat = "BTCPay", + KeyPath = GenerateWalletResponseV.AccountKeyPath.KeyPath.ToString(), DerivationScheme = DerivationScheme.ToString(), Confirmation = true }, cryptoCode); - return new WalletId(StoreId, cryptoCode); } - public DerivationStrategyBase DerivationScheme { get; set; } + public GenerateWalletResponse GenerateWalletResponseV { get; set; } + + public DerivationStrategyBase DerivationScheme + { + get + { + return GenerateWalletResponseV.DerivationScheme; + } + } private async Task RegisterAsync() { diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index d4b7840f4..679cace2b 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -150,17 +150,19 @@ namespace BTCPayServer.Tests new BitcoinLikePaymentHandler(null, networkProvider, null, null), new LightningLikePaymentHandler(null, null, networkProvider, null), }); + var networkBTC = networkProvider.GetNetwork("BTC"); + var networkLTC = networkProvider.GetNetwork("LTC"); InvoiceEntity invoiceEntity = new InvoiceEntity(); invoiceEntity.Networks = networkProvider; invoiceEntity.Payments = new System.Collections.Generic.List(); invoiceEntity.ProductInformation = new ProductInformation() {Price = 100}; PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary(); - paymentMethods.Add(new PaymentMethod() {CryptoCode = "BTC", Rate = 10513.44m,}.SetPaymentMethodDetails( + paymentMethods.Add(new PaymentMethod() {Network = networkBTC, CryptoCode = "BTC", Rate = 10513.44m,}.SetPaymentMethodDetails( new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod() { NextNetworkFee = Money.Coins(0.00000100m), DepositAddress = dummy })); - paymentMethods.Add(new PaymentMethod() {CryptoCode = "LTC", Rate = 216.79m}.SetPaymentMethodDetails( + paymentMethods.Add(new PaymentMethod() {Network = networkLTC, CryptoCode = "LTC", Rate = 216.79m}.SetPaymentMethodDetails( new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod() { NextNetworkFee = Money.Coins(0.00010000m), DepositAddress = dummy diff --git a/BTCPayServer/Configuration/ExternalConnectionString.cs b/BTCPayServer/Configuration/ExternalConnectionString.cs index 8bb83a430..487ce4966 100644 --- a/BTCPayServer/Configuration/ExternalConnectionString.cs +++ b/BTCPayServer/Configuration/ExternalConnectionString.cs @@ -145,9 +145,7 @@ namespace BTCPayServer.Configuration } public bool? IsOnion() { - if (this.Server == null || !this.Server.IsAbsoluteUri) - return null; - return this.Server.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase); + return Server?.IsOnion(); } public static bool TryParse(string str, out ExternalConnectionString result, out string error) { diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 4c300c1b0..19e61beec 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -186,11 +186,19 @@ namespace BTCPayServer public static bool IsSegwit(this DerivationStrategyBase derivationStrategyBase) { - if (IsSegwitCore(derivationStrategyBase)) - return true; - return (derivationStrategyBase is P2SHDerivationStrategy p2shStrat && IsSegwitCore(p2shStrat.Inner)); + return ScriptPubKeyType(derivationStrategyBase) != NBitcoin.ScriptPubKeyType.Legacy; } + public static ScriptPubKeyType ScriptPubKeyType(this DerivationStrategyBase derivationStrategyBase) + { + if (IsSegwitCore(derivationStrategyBase)) + { + return NBitcoin.ScriptPubKeyType.Segwit; + } + return (derivationStrategyBase is P2SHDerivationStrategy p2shStrat && IsSegwitCore(p2shStrat.Inner)) + ? NBitcoin.ScriptPubKeyType.SegwitP2SH + : NBitcoin.ScriptPubKeyType.Legacy; + } private static bool IsSegwitCore(DerivationStrategyBase derivationStrategyBase) { return (derivationStrategyBase is P2WSHDerivationStrategy) || @@ -257,6 +265,12 @@ namespace BTCPayServer return false; return request.Host.Host.EndsWith(".onion", StringComparison.OrdinalIgnoreCase); } + + public static bool IsOnion(this Uri uri) + { + return uri?.DnsSafeHost?.EndsWith(".onion", StringComparison.OrdinalIgnoreCase) is true; + } + public static string GetAbsoluteRoot(this HttpRequest request) { diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 890eb47e9..be19bd8fe 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -260,10 +260,20 @@ namespace BTCPayServer.Hosting { options.AddPolicy(CorsPolicies.All, p => p.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()); }); - - var rateLimits = new RateLimitService(); - rateLimits.SetZone($"zone={ZoneLimits.Login} rate=5r/min burst=3 nodelay"); - services.AddSingleton(rateLimits); + services.AddSingleton(provider => + { + var btcPayEnv = provider.GetService(); + var rateLimits = new RateLimitService(); + if (btcPayEnv.IsDevelopping) + { + rateLimits.SetZone($"zone={ZoneLimits.Login} rate=1000r/min burst=100 nodelay"); + } + else + { + rateLimits.SetZone($"zone={ZoneLimits.Login} rate=5r/min burst=3 nodelay"); + } + return rateLimits; + }); services.AddLogging(logBuilder => diff --git a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs index 9f44b61c4..2fdb72177 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using BTCPayServer.Data; using BTCPayServer.Payments; +using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Services.Invoices; using NBitcoin; using Newtonsoft.Json; @@ -21,6 +22,7 @@ namespace BTCPayServer.Models.InvoicingModels public string TransactionLink { get; set; } public bool Replaced { get; set; } + public BitcoinLikePaymentData CryptoPaymentData { get; set; } } public class OffChainPaymentViewModel diff --git a/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs b/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs index cbbd8c30c..461d189e7 100644 --- a/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs +++ b/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs @@ -31,9 +31,14 @@ namespace BTCPayServer.Payments return ((BTCPayNetwork) network).ToString(paymentData); } - public override IPaymentMethodDetails DeserializePaymentMethodDetails(string str) + public override IPaymentMethodDetails DeserializePaymentMethodDetails(BTCPayNetworkBase network, string str) { - return JsonConvert.DeserializeObject(str); + return JsonConvert.DeserializeObject(str); + } + + public override string SerializePaymentMethodDetails(BTCPayNetworkBase network, IPaymentMethodDetails details) + { + return JsonConvert.SerializeObject(details); } public override ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value) diff --git a/BTCPayServer/Payments/PaymentTypes.Lightning.cs b/BTCPayServer/Payments/PaymentTypes.Lightning.cs index 15388955e..2825fa17f 100644 --- a/BTCPayServer/Payments/PaymentTypes.Lightning.cs +++ b/BTCPayServer/Payments/PaymentTypes.Lightning.cs @@ -29,11 +29,16 @@ namespace BTCPayServer.Payments return ((BTCPayNetwork) network).ToString(paymentData); } - public override IPaymentMethodDetails DeserializePaymentMethodDetails(string str) + public override IPaymentMethodDetails DeserializePaymentMethodDetails(BTCPayNetworkBase network, string str) { return JsonConvert.DeserializeObject(str); } + public override string SerializePaymentMethodDetails(BTCPayNetworkBase network, IPaymentMethodDetails details) + { + return JsonConvert.SerializeObject(details); + } + public override ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value) { return JsonConvert.DeserializeObject(value.ToString()); diff --git a/BTCPayServer/Payments/PaymentTypes.cs b/BTCPayServer/Payments/PaymentTypes.cs index bb5088bc9..229364865 100644 --- a/BTCPayServer/Payments/PaymentTypes.cs +++ b/BTCPayServer/Payments/PaymentTypes.cs @@ -62,7 +62,8 @@ namespace BTCPayServer.Payments public abstract string GetId(); public abstract CryptoPaymentData DeserializePaymentData(BTCPayNetworkBase network, string str); public abstract string SerializePaymentData(BTCPayNetworkBase network, CryptoPaymentData paymentData); - public abstract IPaymentMethodDetails DeserializePaymentMethodDetails(string str); + public abstract IPaymentMethodDetails DeserializePaymentMethodDetails(BTCPayNetworkBase network, string str); + public abstract string SerializePaymentMethodDetails(BTCPayNetworkBase network, IPaymentMethodDetails details); public abstract ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value); public abstract string GetTransactionLink(BTCPayNetworkBase network, string txId); public abstract string InvoiceViewPaymentPartialName { get; } diff --git a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentType.cs b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentType.cs index 0dca0dc10..924d7e07a 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentType.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentType.cs @@ -24,11 +24,16 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments return JsonConvert.SerializeObject(paymentData); } - public override IPaymentMethodDetails DeserializePaymentMethodDetails(string str) + public override IPaymentMethodDetails DeserializePaymentMethodDetails(BTCPayNetworkBase network, string str) { return JsonConvert.DeserializeObject(str); } + public override string SerializePaymentMethodDetails(BTCPayNetworkBase network, IPaymentMethodDetails details) + { + return JsonConvert.SerializeObject(details); + } + public override ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value) { return JsonConvert.DeserializeObject(value.ToString()); diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 5576eed58..ac82730fb 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -792,7 +792,7 @@ namespace BTCPayServer.Services.Invoices } else { - IPaymentMethodDetails details = GetId().PaymentType.DeserializePaymentMethodDetails(PaymentMethodDetails.ToString()); + IPaymentMethodDetails details = GetId().PaymentType.DeserializePaymentMethodDetails(Network, PaymentMethodDetails.ToString()); if (details is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod btcLike) { btcLike.NextNetworkFee = NextNetworkFee; @@ -821,8 +821,7 @@ namespace BTCPayServer.Services.Invoices FeeRate = bitcoinPaymentMethod.FeeRate; DepositAddress = bitcoinPaymentMethod.DepositAddress; } - var jobj = JObject.Parse(JsonConvert.SerializeObject(paymentMethod)); - PaymentMethodDetails = jobj; + PaymentMethodDetails = JObject.Parse(paymentMethod.GetPaymentType().SerializePaymentMethodDetails(Network, paymentMethod)); #pragma warning restore CS0618 // Type or member is obsolete return this; diff --git a/BTCPayServer/Services/SettingsRepository.cs b/BTCPayServer/Services/SettingsRepository.cs index fdd76de52..1afef773d 100644 --- a/BTCPayServer/Services/SettingsRepository.cs +++ b/BTCPayServer/Services/SettingsRepository.cs @@ -19,9 +19,9 @@ namespace BTCPayServer.Services _EventAggregator = eventAggregator; } - public async Task GetSettingAsync() + public async Task GetSettingAsync(string name = null) { - var name = typeof(T).FullName; + name ??= typeof(T).FullName; using (var ctx = _ContextFactory.CreateContext()) { var data = await ctx.Settings.Where(s => s.Id == name).FirstOrDefaultAsync(); @@ -31,9 +31,9 @@ namespace BTCPayServer.Services } } - public async Task UpdateSetting(T obj) + public async Task UpdateSetting(T obj, string name = null) { - var name = obj.GetType().FullName; + name ??= obj.GetType().FullName; using (var ctx = _ContextFactory.CreateContext()) { var settings = new SettingData(); diff --git a/BTCPayServer/Views/Shared/ViewBitcoinLikePaymentData.cshtml b/BTCPayServer/Views/Shared/ViewBitcoinLikePaymentData.cshtml index d6626082d..d30391173 100644 --- a/BTCPayServer/Views/Shared/ViewBitcoinLikePaymentData.cshtml +++ b/BTCPayServer/Views/Shared/ViewBitcoinLikePaymentData.cshtml @@ -26,6 +26,7 @@ m.ReceivedTime = payment.ReceivedTime; m.TransactionLink = string.Format(CultureInfo.InvariantCulture, payment.Network.BlockExplorerLink, m.TransactionId); m.Replaced = !payment.Accounted; + m.CryptoPaymentData = onChainPaymentData; return m; }); } @@ -40,6 +41,7 @@ Crypto Deposit address + Amount Transaction Id Confirmations @@ -50,6 +52,7 @@ @payment.Crypto @payment.DepositAddress + @payment.CryptoPaymentData.GetValue()
diff --git a/BTCPayServer/wwwroot/js/WalletSend.js b/BTCPayServer/wwwroot/js/WalletSend.js index 7976ae3b7..3da3deea2 100644 --- a/BTCPayServer/wwwroot/js/WalletSend.js +++ b/BTCPayServer/wwwroot/js/WalletSend.js @@ -25,8 +25,6 @@ function updateFiatValueWithCurrentElement() { } $(function () { - - $(".output-amount").on("input", updateFiatValueWithCurrentElement).each(updateFiatValueWithCurrentElement); $("#crypto-fee-link").on("click", function (elem) { From 6d0896084fe8bc66518d59f8c93b36dce46f10f8 Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Wed, 19 Feb 2020 09:39:14 +0100 Subject: [PATCH 201/810] Add JS Modal test (#1342) --- BTCPayServer.Tests/CheckoutUITests.cs | 40 +++++++++++++++++++ .../Views/Invoice/Checkout-Body.cshtml | 4 +- BTCPayServer/wwwroot/tests/index.html | 20 ++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 BTCPayServer/wwwroot/tests/index.html diff --git a/BTCPayServer.Tests/CheckoutUITests.cs b/BTCPayServer.Tests/CheckoutUITests.cs index b6c115c93..fc7730e18 100644 --- a/BTCPayServer.Tests/CheckoutUITests.cs +++ b/BTCPayServer.Tests/CheckoutUITests.cs @@ -4,8 +4,10 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Lightning; +using BTCPayServer.Payments; using BTCPayServer.Tests.Logging; using BTCPayServer.Views.Stores; +using NBitcoin; using NBitpayClient; using OpenQA.Selenium; using OpenQA.Selenium.Interactions; @@ -177,5 +179,43 @@ namespace BTCPayServer.Tests } } + + [Fact(Timeout = TestTimeout)] + public async Task CanUseJSModal() + { + using (var s = SeleniumTester.Create()) + { + await s.StartAsync(); + s.GoToRegister(); + s.RegisterNewUser(); + var store = s.CreateNewStore(); + s.GoToStore(store.storeId); + s.AddDerivationScheme(); + var invoiceId = s.CreateInvoice(store.storeId, 0.001m, "BTC", "a@x.com"); + var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId); + s.Driver.Navigate() + .GoToUrl(new Uri(s.Server.PayTester.ServerUri, $"tests/index.html?invoice={invoiceId}")); + TestUtils.Eventually(() => + { + Assert.True(s.Driver.FindElement(By.Name("btcpay")).Displayed); + }); + await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(invoice + .GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)) + .GetPaymentMethodDetails().GetPaymentDestination(), Network.RegTest), + new Money(0.001m, MoneyUnit.BTC)); + + IWebElement closebutton = null; + TestUtils.Eventually(() => + { + var iframe = s.Driver.SwitchTo().Frame("btcpay"); + closebutton = iframe.FindElement(By.ClassName("close-action")); + Assert.True(closebutton.Displayed); + }); + closebutton.Click(); + s.Driver.AssertElementNotFound(By.Name("btcpay")); + Assert.Equal(s.Driver.Url, + new Uri(s.Server.PayTester.ServerUri, $"tests/index.html?invoice={invoiceId}").ToString()); + } + } } } diff --git a/BTCPayServer/Views/Invoice/Checkout-Body.cshtml b/BTCPayServer/Views/Invoice/Checkout-Body.cshtml index c17c81f70..e872b32e3 100644 --- a/BTCPayServer/Views/Invoice/Checkout-Body.cshtml +++ b/BTCPayServer/Views/Invoice/Checkout-Body.cshtml @@ -208,7 +208,7 @@
@@ -241,7 +241,7 @@ {{$t("Return to StoreName", srvModel)}}
diff --git a/BTCPayServer/wwwroot/tests/index.html b/BTCPayServer/wwwroot/tests/index.html new file mode 100644 index 000000000..064d66883 --- /dev/null +++ b/BTCPayServer/wwwroot/tests/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + From dc4f8a1fbe841c0824d545582cc46ae533e955c4 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Wed, 19 Feb 2020 22:03:59 +0900 Subject: [PATCH 202/810] Sort invoice list (Fix #1329) --- BTCPayServer/Services/Invoices/InvoiceRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 59f5d52b5..2b7b6cd53 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -606,7 +606,7 @@ retry: if (queryObject.IncludeEvents) query = query.Include(o => o.Events); - var data = await query.ToArrayAsync().ConfigureAwait(false); + var data = await query.OrderByDescending(i => i.Created).ToArrayAsync().ConfigureAwait(false); return data.Select(ToEntity).ToArray(); } } From b79b310bd5fbbc5bfd8aca49ac65cb59e65b1a0a Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 21 Feb 2020 11:29:09 +0900 Subject: [PATCH 203/810] Revert "Sort invoice list (Fix #1329)" This reverts commit dc4f8a1fbe841c0824d545582cc46ae533e955c4. --- BTCPayServer/Services/Invoices/InvoiceRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 2b7b6cd53..59f5d52b5 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -606,7 +606,7 @@ retry: if (queryObject.IncludeEvents) query = query.Include(o => o.Events); - var data = await query.OrderByDescending(i => i.Created).ToArrayAsync().ConfigureAwait(false); + var data = await query.ToArrayAsync().ConfigureAwait(false); return data.Select(ToEntity).ToArray(); } } From 34702d2633dca1b52b87f4e3552e5483eb12c9f1 Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Fri, 21 Feb 2020 05:40:00 +0100 Subject: [PATCH 204/810] Revoke Legacy Api Keys (#1344) closes #1333 --- BTCPayServer/Controllers/StoresController.cs | 16 ++++++++++++--- .../Security/Bitpay/TokenRepository.cs | 15 ++++++++++++++ BTCPayServer/Views/Stores/ListTokens.cshtml | 20 +++++++++++++++++-- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 99bec985b..dda5a65ee 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -647,6 +647,7 @@ namespace BTCPayServer.Controllers return View(model); } + [HttpGet] [Route("{storeId}/tokens/{tokenId}/revoke")] public async Task RevokeToken(string tokenId) @@ -780,13 +781,22 @@ namespace BTCPayServer.Controllers [HttpPost] [Route("{storeId}/tokens/apikey")] - public async Task GenerateAPIKey(string storeId) + public async Task GenerateAPIKey(string storeId, string command="") { var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); - await _TokenRepository.GenerateLegacyAPIKey(CurrentStore.Id); - TempData[WellKnownTempData.SuccessMessage] = "API Key re-generated"; + if (command == "revoke") + { + await _TokenRepository.RevokeLegacyAPIKeys(CurrentStore.Id); + TempData[WellKnownTempData.SuccessMessage] = "API Key revoked"; + } + else + { + await _TokenRepository.GenerateLegacyAPIKey(CurrentStore.Id); + TempData[WellKnownTempData.SuccessMessage] = "API Key re-generated"; + } + return RedirectToAction(nameof(ListTokens), new { storeId diff --git a/BTCPayServer/Security/Bitpay/TokenRepository.cs b/BTCPayServer/Security/Bitpay/TokenRepository.cs index c52acbf34..cb0289a52 100644 --- a/BTCPayServer/Security/Bitpay/TokenRepository.cs +++ b/BTCPayServer/Security/Bitpay/TokenRepository.cs @@ -76,6 +76,21 @@ namespace BTCPayServer.Security.Bitpay } } + public async Task RevokeLegacyAPIKeys(string storeId) + { + var keys = await GetLegacyAPIKeys(storeId); + if (!keys.Any()) + { + return; + } + + using (var ctx = _Factory.CreateContext()) + { + ctx.ApiKeys.RemoveRange(keys.Select(s => new APIKeyData() {Id = s})); + await ctx.SaveChangesAsync(); + } + } + public async Task GetLegacyAPIKeys(string storeId) { using (var ctx = _Factory.CreateContext()) diff --git a/BTCPayServer/Views/Stores/ListTokens.cshtml b/BTCPayServer/Views/Stores/ListTokens.cshtml index 2af62ceb4..d7ce9d040 100644 --- a/BTCPayServer/Views/Stores/ListTokens.cshtml +++ b/BTCPayServer/Views/Stores/ListTokens.cshtml @@ -55,9 +55,25 @@
- +
+ + @if (string.IsNullOrEmpty(Model.ApiKey)) + { +
+ +
+ } + else + { +
+ +
+
+ +
+ } +
-
From 1be64082469b44982f851d7c09e35edca1b17c4d Mon Sep 17 00:00:00 2001 From: rockstardev Date: Thu, 20 Feb 2020 16:54:18 -0600 Subject: [PATCH 205/810] Adding logging to try and catch situations where invoice is not expired --- BTCPayServer/HostedServices/InvoiceWatcher.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/BTCPayServer/HostedServices/InvoiceWatcher.cs b/BTCPayServer/HostedServices/InvoiceWatcher.cs index fffc5453e..85e62f071 100644 --- a/BTCPayServer/HostedServices/InvoiceWatcher.cs +++ b/BTCPayServer/HostedServices/InvoiceWatcher.cs @@ -180,7 +180,11 @@ namespace BTCPayServer.HostedServices { if (invoiceId == null) throw new ArgumentNullException(nameof(invoiceId)); - _WatchRequests.Writer.TryWrite(invoiceId); + + if (!_WatchRequests.Writer.TryWrite(invoiceId)) + { + Logs.PayServer.LogWarning($"Failed to write invoice {invoiceId} into WatchRequests channel"); + } } private async Task Wait(string invoiceId) From 29d51ad2a2cdfad3e770b450255ecd6760731cbe Mon Sep 17 00:00:00 2001 From: rockstardev Date: Fri, 21 Feb 2020 02:14:49 -0600 Subject: [PATCH 206/810] Adding 1 second leeway for expiration --- BTCPayServer/HostedServices/InvoiceWatcher.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/BTCPayServer/HostedServices/InvoiceWatcher.cs b/BTCPayServer/HostedServices/InvoiceWatcher.cs index 85e62f071..105daf63e 100644 --- a/BTCPayServer/HostedServices/InvoiceWatcher.cs +++ b/BTCPayServer/HostedServices/InvoiceWatcher.cs @@ -54,7 +54,7 @@ namespace BTCPayServer.HostedServices private async Task UpdateInvoice(UpdateInvoiceContext context) { var invoice = context.Invoice; - if (invoice.Status == InvoiceStatus.New && invoice.ExpirationTime < DateTimeOffset.UtcNow) + if (invoice.Status == InvoiceStatus.New && invoice.ExpirationTime <= DateTimeOffset.UtcNow) { context.MarkDirty(); await _InvoiceRepository.UnaffectAddress(invoice.Id); @@ -192,13 +192,16 @@ namespace BTCPayServer.HostedServices var invoice = await _InvoiceRepository.GetInvoice(invoiceId); try { - var delay = invoice.ExpirationTime - DateTimeOffset.UtcNow; + // add 1 second to ensure watch won't trigger moments before invoice expires + var delay = invoice.ExpirationTime.AddSeconds(1) - DateTimeOffset.UtcNow; if (delay > TimeSpan.Zero) { await Task.Delay(delay, _Cts.Token); } Watch(invoiceId); - delay = invoice.MonitoringExpiration - DateTimeOffset.UtcNow; + + // add 1 second to ensure watch won't trigger moments before monitoring expires + delay = invoice.MonitoringExpiration.AddSeconds(1) - DateTimeOffset.UtcNow; if (delay > TimeSpan.Zero) { await Task.Delay(delay, _Cts.Token); From a354f7d9dd94be40ffe7f7f67b410512bb4009dd Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Mon, 24 Feb 2020 13:18:04 +0100 Subject: [PATCH 207/810] add GET endpoint for pay button (#1349) closes #889 --- BTCPayServer/Controllers/PublicController.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/BTCPayServer/Controllers/PublicController.cs b/BTCPayServer/Controllers/PublicController.cs index cdc8cb8b6..5c0dea090 100644 --- a/BTCPayServer/Controllers/PublicController.cs +++ b/BTCPayServer/Controllers/PublicController.cs @@ -26,6 +26,15 @@ namespace BTCPayServer.Controllers private InvoiceController _InvoiceController; private StoreRepository _StoreRepository; + [HttpGet] + [IgnoreAntiforgeryToken] + [EnableCors(CorsPolicies.All)] + [Route("api/v1/invoices")] + public async Task PayButtonHandle(PayButtonViewModel model) + { + return await PayButtonHandle(model, CancellationToken.None); + } + [HttpPost] [Route("api/v1/invoices")] [MediaTypeAcceptConstraintAttribute("text/html")] From f097ecdc80dfd341d06ffcc13b6eb1910971188c Mon Sep 17 00:00:00 2001 From: hannes Date: Mon, 24 Feb 2020 20:21:03 +0800 Subject: [PATCH 208/810] fix: remove ipn via email #1241 (#1337) * fix: remove ipn via email #1241 * fix: remove ipn via email #1241 --- BTCPayServer/Controllers/AppsController.Crowdfund.cs | 2 -- BTCPayServer/Controllers/AppsController.PointOfSale.cs | 4 ---- BTCPayServer/Controllers/AppsPublicController.cs | 2 -- BTCPayServer/Controllers/InvoiceController.UI.cs | 2 -- .../HostedServices/InvoiceNotificationManager.cs | 1 + .../Models/AppViewModels/UpdateCrowdfundViewModel.cs | 5 ----- .../Models/AppViewModels/UpdatePointOfSaleViewModel.cs | 4 ---- .../Models/InvoicingModels/CreateInvoiceModel.cs | 7 ------- BTCPayServer/Services/Apps/CrowdfundSettings.cs | 2 -- BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml | 10 ---------- BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml | 10 ---------- BTCPayServer/Views/Invoice/CreateInvoice.cshtml | 5 ----- BTCPayServer/Views/Invoice/Invoice.cshtml | 4 ---- 13 files changed, 1 insertion(+), 57 deletions(-) diff --git a/BTCPayServer/Controllers/AppsController.Crowdfund.cs b/BTCPayServer/Controllers/AppsController.Crowdfund.cs index 98d40b0a6..16c8288fe 100644 --- a/BTCPayServer/Controllers/AppsController.Crowdfund.cs +++ b/BTCPayServer/Controllers/AppsController.Crowdfund.cs @@ -34,7 +34,6 @@ namespace BTCPayServer.Controllers var settings = app.GetSettings(); var vm = new UpdateCrowdfundViewModel() { - NotificationEmailWarning = !await IsEmailConfigured(app.StoreDataId), Title = settings.Title, StoreId = app.StoreDataId, Enabled = settings.Enabled, @@ -139,7 +138,6 @@ namespace BTCPayServer.Controllers MainImageUrl = vm.MainImageUrl, EmbeddedCSS = vm.EmbeddedCSS, NotificationUrl = vm.NotificationUrl, - NotificationEmail = vm.NotificationEmail, Tagline = vm.Tagline, PerksTemplate = vm.PerksTemplate, DisqusEnabled = vm.DisqusEnabled, diff --git a/BTCPayServer/Controllers/AppsController.PointOfSale.cs b/BTCPayServer/Controllers/AppsController.PointOfSale.cs index 53c6eadff..792920111 100644 --- a/BTCPayServer/Controllers/AppsController.PointOfSale.cs +++ b/BTCPayServer/Controllers/AppsController.PointOfSale.cs @@ -83,7 +83,6 @@ namespace BTCPayServer.Controllers public string EmbeddedCSS { get; set; } public string Description { get; set; } - public string NotificationEmail { get; set; } public string NotificationUrl { get; set; } public bool? RedirectAutomatically { get; set; } } @@ -99,7 +98,6 @@ namespace BTCPayServer.Controllers var vm = new UpdatePointOfSaleViewModel() { - NotificationEmailWarning = !await IsEmailConfigured(app.StoreDataId), Id = appId, StoreId = app.StoreDataId, Title = settings.Title, @@ -116,7 +114,6 @@ namespace BTCPayServer.Controllers CustomCSSLink = settings.CustomCSSLink, EmbeddedCSS = settings.EmbeddedCSS, Description = settings.Description, - NotificationEmail = settings.NotificationEmail, NotificationUrl = settings.NotificationUrl, SearchTerm = $"storeid:{app.StoreDataId}", RedirectAutomatically = settings.RedirectAutomatically.HasValue ? settings.RedirectAutomatically.Value ? "true" : "false" : "" @@ -194,7 +191,6 @@ namespace BTCPayServer.Controllers CustomTipPercentages = ListSplit(vm.CustomTipPercentages), CustomCSSLink = vm.CustomCSSLink, NotificationUrl = vm.NotificationUrl, - NotificationEmail = vm.NotificationEmail, Description = vm.Description, EmbeddedCSS = vm.EmbeddedCSS, RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? (bool?)null : bool.Parse(vm.RedirectAutomatically) diff --git a/BTCPayServer/Controllers/AppsPublicController.cs b/BTCPayServer/Controllers/AppsPublicController.cs index f4f5d46b1..98c07d9fc 100644 --- a/BTCPayServer/Controllers/AppsPublicController.cs +++ b/BTCPayServer/Controllers/AppsPublicController.cs @@ -177,7 +177,6 @@ namespace BTCPayServer.Controllers OrderId = orderId, NotificationURL = string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl, - NotificationEmail = settings.NotificationEmail, RedirectURL = redirectUrl ?? Request.GetDisplayUrl(), FullNotifications = true, ExtendedNotifications = true, @@ -306,7 +305,6 @@ namespace BTCPayServer.Controllers BuyerEmail = request.Email, Price = price, NotificationURL = settings.NotificationUrl, - NotificationEmail = settings.NotificationEmail, FullNotifications = true, ExtendedNotifications = true, RedirectURL = request.RedirectUrl ?? diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index d5f1b90a7..c1fea7bdc 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -65,7 +65,6 @@ namespace BTCPayServer.Controllers BuyerInformation = invoice.BuyerInformation, Fiat = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.Price, prodInfo.Currency), TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.TaxIncluded, prodInfo.Currency), - NotificationEmail = invoice.NotificationEmail, NotificationUrl = invoice.NotificationURL?.AbsoluteUri, RedirectUrl = invoice.RedirectURL?.AbsoluteUri, ProductInformation = invoice.ProductInformation, @@ -538,7 +537,6 @@ namespace BTCPayServer.Controllers PosData = model.PosData, OrderId = model.OrderId, //RedirectURL = redirect + "redirect", - NotificationEmail = model.NotificationEmail, NotificationURL = model.NotificationUrl, ItemDesc = model.ItemDesc, FullNotifications = true, diff --git a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs index 075a6532d..3c9452b91 100644 --- a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs +++ b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs @@ -128,6 +128,7 @@ namespace BTCPayServer.HostedServices emailBody); } + if (invoice.NotificationURL != null) { var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Notification = notification }); diff --git a/BTCPayServer/Models/AppViewModels/UpdateCrowdfundViewModel.cs b/BTCPayServer/Models/AppViewModels/UpdateCrowdfundViewModel.cs index 7cf676689..83ce70ff5 100644 --- a/BTCPayServer/Models/AppViewModels/UpdateCrowdfundViewModel.cs +++ b/BTCPayServer/Models/AppViewModels/UpdateCrowdfundViewModel.cs @@ -26,9 +26,6 @@ namespace BTCPayServer.Models.AppViewModels [Display(Name = "Callback Notification Url")] [Uri] public string NotificationUrl { get; set; } - [Display(Name = "Invoice IPN Notification")] - [EmailAddress] - public string NotificationEmail { get; set; } [Required] [Display(Name = "Allow crowdfund to be publicly visible (still visible to you)")] @@ -99,8 +96,6 @@ namespace BTCPayServer.Models.AppViewModels [Display(Name = "Colors to rotate between with animation when a payment is made. First color is the default background. One color per line. Can be any valid css color value.")] public string AnimationColors { get; set; } - public bool NotificationEmailWarning { get; set; } - // NOTE: Improve validation if needed public bool ModelWithMinimumData diff --git a/BTCPayServer/Models/AppViewModels/UpdatePointOfSaleViewModel.cs b/BTCPayServer/Models/AppViewModels/UpdatePointOfSaleViewModel.cs index 32d4e4b78..e71910d45 100644 --- a/BTCPayServer/Models/AppViewModels/UpdatePointOfSaleViewModel.cs +++ b/BTCPayServer/Models/AppViewModels/UpdatePointOfSaleViewModel.cs @@ -32,9 +32,6 @@ namespace BTCPayServer.Models.AppViewModels [Display(Name = "Callback Notification Url")] [Uri] public string NotificationUrl { get; set; } - [Display(Name = "Invoice IPN Notification")] - [EmailAddress] - public string NotificationEmail { get; set; } [Required] [MaxLength(30)] @@ -84,7 +81,6 @@ namespace BTCPayServer.Models.AppViewModels } }, nameof(SelectListItem.Value), nameof(SelectListItem.Text), RedirectAutomatically); - public bool NotificationEmailWarning { get; set; } public string EmbeddedCSS { get; set; } public string Description { get; set; } } diff --git a/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs b/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs index 0b8e3c893..92507201c 100644 --- a/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs +++ b/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs @@ -60,13 +60,6 @@ namespace BTCPayServer.Models.InvoicingModels get; set; } - [EmailAddress] - [DisplayName("Notification Email")] - public string NotificationEmail - { - get; set; - } - [Uri] [DisplayName("Notification Url")] public string NotificationUrl diff --git a/BTCPayServer/Services/Apps/CrowdfundSettings.cs b/BTCPayServer/Services/Apps/CrowdfundSettings.cs index d6091a6b9..afb5783df 100644 --- a/BTCPayServer/Services/Apps/CrowdfundSettings.cs +++ b/BTCPayServer/Services/Apps/CrowdfundSettings.cs @@ -82,8 +82,6 @@ namespace BTCPayServer.Services.Apps "//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/unstoppable.wav", "//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/whickedsick.wav" }; - - public string NotificationEmail { get; set; } } public enum CrowdfundResetEvery { diff --git a/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml b/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml index 12a5091af..ef371ca8b 100644 --- a/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml +++ b/BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml @@ -143,15 +143,6 @@
-
- - @if (Model.NotificationEmailWarning) - { - - } - - -
@@ -213,7 +204,6 @@
-
diff --git a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml index 6d3a0c54b..a92662e85 100644 --- a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml +++ b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml @@ -119,15 +119,6 @@
-
- - @if (Model.NotificationEmailWarning) - { - - } - - -
* @@ -143,7 +134,6 @@
-
diff --git a/BTCPayServer/Views/Invoice/CreateInvoice.cshtml b/BTCPayServer/Views/Invoice/CreateInvoice.cshtml index 5ba652967..858a19b7a 100644 --- a/BTCPayServer/Views/Invoice/CreateInvoice.cshtml +++ b/BTCPayServer/Views/Invoice/CreateInvoice.cshtml @@ -52,11 +52,6 @@
-
- - - -
diff --git a/BTCPayServer/Views/Invoice/Invoice.cshtml b/BTCPayServer/Views/Invoice/Invoice.cshtml index f49544c58..d2e5d39c5 100644 --- a/BTCPayServer/Views/Invoice/Invoice.cshtml +++ b/BTCPayServer/Views/Invoice/Invoice.cshtml @@ -77,10 +77,6 @@ Total fiat due @Model.Fiat - - Notification Email - @Model.NotificationEmail - Notification Url @Model.NotificationUrl From ee524e36c5c7895c72afa694c6d010b53265c5d6 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Mon, 24 Feb 2020 21:25:52 +0900 Subject: [PATCH 209/810] bump --- Build/Version.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Build/Version.csproj b/Build/Version.csproj index f5a4f7215..827527c45 100644 --- a/Build/Version.csproj +++ b/Build/Version.csproj @@ -1,5 +1,5 @@ - 1.0.3.158 + 1.0.3.159 From 1d61db47589cf8e0f5ceb06ea40e785369f4137f Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Mon, 24 Feb 2020 13:29:29 +0100 Subject: [PATCH 210/810] Add text customization for pay button (#1346) * Add text customization for pay button https://i.imgur.com/nFxscOZ.gifv * pr changes --- .../StoreViewModels/PayButtonViewModel.cs | 1 + BTCPayServer/Views/Stores/PayButton.cshtml | 63 ++++++++++++++----- BTCPayServer/wwwroot/paybutton/paybutton.js | 15 ++++- 3 files changed, 61 insertions(+), 18 deletions(-) diff --git a/BTCPayServer/Models/StoreViewModels/PayButtonViewModel.cs b/BTCPayServer/Models/StoreViewModels/PayButtonViewModel.cs index ebb0dc362..6c688a26b 100644 --- a/BTCPayServer/Models/StoreViewModels/PayButtonViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/PayButtonViewModel.cs @@ -40,5 +40,6 @@ namespace BTCPayServer.Models.StoreViewModels public string UrlRoot { get; set; } public List CurrencyDropdown { get; set; } public string PayButtonImageUrl { get; set; } + public string PayButtonText { get; set; } } } diff --git a/BTCPayServer/Views/Stores/PayButton.cshtml b/BTCPayServer/Views/Stores/PayButton.cshtml index bcd1f633b..787dfac77 100644 --- a/BTCPayServer/Views/Stores/PayButton.cshtml +++ b/BTCPayServer/Views/Stores/PayButton.cshtml @@ -47,15 +47,26 @@
+
+ +
+
+ + +
+ v-validate="{ required: this.imageUrlRequired, url: {require_tld:false} }" + :class="{'is-invalid': errors.has('payButtonImageUrl') }"> {{ errors.first('payButtonImageUrl') }}
- +
- - - {{ errors.first('fitButtonInline') }} + + + {{ errors.first('fitButtonInline') }}
@@ -232,13 +243,33 @@ var payButtonCtrl = new Vue({ el: '#payButtonCtrl', data: { - srvModel: srvModel + srvModel: srvModel, + originalButtonImageUrl: srvModel.payButtonImageUrl, + buttonInlineTextMode: false + }, + computed: { + imageUrlRequired: function(){ + return !this.buttonInlineTextMode; + } }, methods: { inputChanges: function (event, buttonSize) { inputChanges(event, buttonSize); } - } + + }, + watch: { + buttonInlineTextMode: function (checked) { + if(!checked){ + this.srvModel.payButtonText = ""; + this.srvModel.payButtonImageUrl = this.originalButtonImageUrl; + }else { + this.srvModel.payButtonText = "Pay with.."; + this.srvModel.payButtonImageUrl = this.srvModel.urlRoot + "img/logo.svg"; + } + this.inputChanges(); + } + } }); } diff --git a/BTCPayServer/wwwroot/paybutton/paybutton.js b/BTCPayServer/wwwroot/paybutton/paybutton.js index dc445186a..342bbba9e 100644 --- a/BTCPayServer/wwwroot/paybutton/paybutton.js +++ b/BTCPayServer/wwwroot/paybutton/paybutton.js @@ -51,14 +51,18 @@ function inputChanges(event, buttonSize) { var isSlider = srvModel.buttonType == 2 var width = "209px"; + var height = "57px"; var widthInput = "3em"; if (srvModel.buttonSize === 0) { width = "146px"; widthInput = "2em"; + height = "40px"; } else if (srvModel.buttonSize === 1) { width = "168px"; + height = "46px"; } else if (srvModel.buttonSize === 2) { width = "209px"; + height = "57px"; } var html = @@ -103,8 +107,15 @@ function inputChanges(event, buttonSize) { html += addSlider(srvModel.price, srvModel.min, srvModel.max, srvModel.step, width); html += '
\n'; } - - html += ' \n'; + + if(!srvModel.payButtonText){ + html += ' \n'; + }else{ + var numwidth = parseInt(width.replace("px", "")); + html+= '' + } html += ''; $("#mainCode").text(html).html(); From 2a7f6e4aa30e5dc08264cc64204e7fa8e05193a2 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Mon, 24 Feb 2020 21:32:54 +0900 Subject: [PATCH 211/810] bump --- Build/Version.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Build/Version.csproj b/Build/Version.csproj index 827527c45..5c813fd8b 100644 --- a/Build/Version.csproj +++ b/Build/Version.csproj @@ -1,5 +1,5 @@ - 1.0.3.159 + 1.0.3.160 From a3e7729c52e6e84b897abcc030bb6ccb1355ee08 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Mon, 24 Feb 2020 22:12:50 +0900 Subject: [PATCH 212/810] Remove warnings --- BTCPayServer/Controllers/WalletsController.PSBT.cs | 2 +- BTCPayServer/Controllers/WalletsController.cs | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/BTCPayServer/Controllers/WalletsController.PSBT.cs b/BTCPayServer/Controllers/WalletsController.PSBT.cs index e30d5ec7c..d4cab9b3a 100644 --- a/BTCPayServer/Controllers/WalletsController.PSBT.cs +++ b/BTCPayServer/Controllers/WalletsController.PSBT.cs @@ -117,7 +117,7 @@ namespace BTCPayServer.Controllers .GetMetadataAsync(derivationScheme.AccountDerivation, WellknownMetadataKeys.MasterHDKey); - return await SignWithSeed(walletId, + return SignWithSeed(walletId, new SignWithSeedViewModel() {SeedOrKey = extKey, PSBT = psbt.ToBase64()}); } diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index 0f2d63d5d..e4dc74e6d 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -573,7 +573,7 @@ namespace BTCPayServer.Controllers var extKey = await ExplorerClientProvider.GetExplorerClient(network) .GetMetadataAsync(derivationScheme.AccountDerivation, WellknownMetadataKeys.MasterHDKey, cancellation); - return await SignWithSeed(walletId, new SignWithSeedViewModel() + return SignWithSeed(walletId, new SignWithSeedViewModel() { SeedOrKey = extKey, PSBT = psbt.PSBT.ToBase64() @@ -645,10 +645,9 @@ namespace BTCPayServer.Controllers [HttpPost] [Route("{walletId}/vault")] - public async Task SubmitVault([ModelBinder(typeof(WalletIdModelBinder))] + public IActionResult SubmitVault([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, WalletSendVaultModel model) { - return RedirectToWalletPSBTReady(model.PSBT); } private IActionResult RedirectToWalletPSBTReady(string psbt, string signingKey= null, string signingKeyPath = null) @@ -718,7 +717,7 @@ namespace BTCPayServer.Controllers [HttpPost] [Route("{walletId}/ledger")] - public async Task SubmitLedger([ModelBinder(typeof(WalletIdModelBinder))] + public IActionResult SubmitLedger([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, WalletSendLedgerModel model) { return RedirectToWalletPSBTReady(model.PSBT); @@ -735,7 +734,7 @@ namespace BTCPayServer.Controllers } [HttpPost("{walletId}/psbt/seed")] - public async Task SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))] + public IActionResult SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, SignWithSeedViewModel viewModel) { if (!ModelState.IsValid) From fa51180dfa91c3c9bfa1983647ded233fc0023c2 Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Mon, 24 Feb 2020 14:36:15 +0100 Subject: [PATCH 213/810] Api keys with openiddict (#1262) * Remove OpenIddict * Add API Key system * Revert removing OpenIddict * fix rebase * fix tests * pr changes * fix tests * fix apikey test * pr change * fix db * add migration attrs * fix migration error * PR Changes * Fix sqlite migration * change api key to use Authorization Header * add supportAddForeignKey * use tempdata status message * fix add api key css * remove redirect url + app identifier feature :( --- BTCPayServer.Data/Data/APIKeyData.cs | 30 +- .../Data/ApplicationDbContext.cs | 6 + BTCPayServer.Data/Data/ApplicationUser.cs | 1 + .../20200119130108_ExtendApiKeys.cs | 74 +++++ .../ApplicationDbContextModelSnapshot.cs | 17 + BTCPayServer.Data/MigrationsExtensions.cs | 5 +- BTCPayServer.Tests/ApiKeysTests.cs | 300 ++++++++++++++++++ BTCPayServer.Tests/AuthenticationTests.cs | 19 +- BTCPayServer.Tests/SeleniumTester.cs | 26 +- .../Controllers/ManageController.APIKeys.cs | 295 +++++++++++++++++ BTCPayServer/Controllers/ManageController.cs | 11 +- .../RestApi/TestApiKeyController.cs | 73 +++++ ...tController.cs => TestOpenIdController.cs} | 7 +- BTCPayServer/Hosting/BTCPayServerServices.cs | 15 +- .../APIKeys/APIKeyAuthenticationHandler.cs | 56 ++++ .../APIKeys/APIKeyAuthenticationOptions.cs | 8 + .../APIKeys/APIKeyAuthorizationHandler.cs | 84 +++++ .../Security/APIKeys/APIKeyConstants.cs | 36 +++ .../Security/APIKeys/APIKeyExtensions.cs | 75 +++++ .../Security/APIKeys/APIKeyRepository.cs | 72 +++++ .../Security/AuthenticationSchemes.cs | 1 + .../Security/Bitpay/TokenRepository.cs | 8 +- BTCPayServer/Security/Policies.cs | 5 + .../Services/Stores/StoreRepository.cs | 4 +- BTCPayServer/Views/Manage/APIKeys.cshtml | 50 +++ BTCPayServer/Views/Manage/AddApiKey.cshtml | 120 +++++++ .../Views/Manage/AuthorizeAPIKey.cshtml | 137 ++++++++ BTCPayServer/Views/Manage/ManageNavPages.cs | 2 +- BTCPayServer/Views/Manage/_Nav.cshtml | 9 +- 29 files changed, 1502 insertions(+), 44 deletions(-) create mode 100644 BTCPayServer.Data/Migrations/20200119130108_ExtendApiKeys.cs create mode 100644 BTCPayServer.Tests/ApiKeysTests.cs create mode 100644 BTCPayServer/Controllers/ManageController.APIKeys.cs create mode 100644 BTCPayServer/Controllers/RestApi/TestApiKeyController.cs rename BTCPayServer/Controllers/RestApi/{TestController.cs => TestOpenIdController.cs} (90%) create mode 100644 BTCPayServer/Security/APIKeys/APIKeyAuthenticationHandler.cs create mode 100644 BTCPayServer/Security/APIKeys/APIKeyAuthenticationOptions.cs create mode 100644 BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs create mode 100644 BTCPayServer/Security/APIKeys/APIKeyConstants.cs create mode 100644 BTCPayServer/Security/APIKeys/APIKeyExtensions.cs create mode 100644 BTCPayServer/Security/APIKeys/APIKeyRepository.cs create mode 100644 BTCPayServer/Views/Manage/APIKeys.cshtml create mode 100644 BTCPayServer/Views/Manage/AddApiKey.cshtml create mode 100644 BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml diff --git a/BTCPayServer.Data/Data/APIKeyData.cs b/BTCPayServer.Data/Data/APIKeyData.cs index b1a27af6d..715d3f2ec 100644 --- a/BTCPayServer.Data/Data/APIKeyData.cs +++ b/BTCPayServer.Data/Data/APIKeyData.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Threading.Tasks; @@ -11,15 +13,31 @@ namespace BTCPayServer.Data [MaxLength(50)] public string Id { - get; set; + get; + set; } - [MaxLength(50)] - public string StoreId - { - get; set; - } + [MaxLength(50)] public string StoreId { get; set; } + [MaxLength(50)] public string UserId { get; set; } + + public APIKeyType Type { get; set; } = APIKeyType.Legacy; + public string Permissions { get; set; } + public StoreData StoreData { get; set; } + public ApplicationUser User { get; set; } + public string[] GetPermissions() { return Permissions?.Split(';') ?? new string[0]; } + + public void SetPermissions(IEnumerable permissions) + { + Permissions = string.Join(';', + permissions?.Select(s => s.Replace(";", string.Empty)) ?? new string[0]); + } + } + + public enum APIKeyType + { + Legacy, + Permanent } } diff --git a/BTCPayServer.Data/Data/ApplicationDbContext.cs b/BTCPayServer.Data/Data/ApplicationDbContext.cs index a2a24b223..e245b85bc 100644 --- a/BTCPayServer.Data/Data/ApplicationDbContext.cs +++ b/BTCPayServer.Data/Data/ApplicationDbContext.cs @@ -160,6 +160,12 @@ namespace BTCPayServer.Data .HasOne(o => o.StoreData) .WithMany(i => i.APIKeys) .HasForeignKey(i => i.StoreId).OnDelete(DeleteBehavior.Cascade); + + builder.Entity() + .HasOne(o => o.User) + .WithMany(i => i.APIKeys) + .HasForeignKey(i => i.UserId).OnDelete(DeleteBehavior.Cascade); + builder.Entity() .HasIndex(o => o.StoreId); diff --git a/BTCPayServer.Data/Data/ApplicationUser.cs b/BTCPayServer.Data/Data/ApplicationUser.cs index fd2b1b5d2..1286e147c 100644 --- a/BTCPayServer.Data/Data/ApplicationUser.cs +++ b/BTCPayServer.Data/Data/ApplicationUser.cs @@ -30,5 +30,6 @@ namespace BTCPayServer.Data } public List U2FDevices { get; set; } + public List APIKeys { get; set; } } } diff --git a/BTCPayServer.Data/Migrations/20200119130108_ExtendApiKeys.cs b/BTCPayServer.Data/Migrations/20200119130108_ExtendApiKeys.cs new file mode 100644 index 000000000..9eeb5b83c --- /dev/null +++ b/BTCPayServer.Data/Migrations/20200119130108_ExtendApiKeys.cs @@ -0,0 +1,74 @@ +using BTCPayServer.Data; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace BTCPayServer.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20200119130108_ExtendApiKeys")] + public partial class ExtendApiKeys : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Permissions", + table: "ApiKeys", + nullable: true); + + migrationBuilder.AddColumn( + name: "Type", + table: "ApiKeys", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "UserId", + table: "ApiKeys", + maxLength: 50, + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeys_UserId", + table: "ApiKeys", + column: "UserId"); + if (this.SupportAddForeignKey(migrationBuilder.ActiveProvider)) + { + migrationBuilder.AddForeignKey( + name: "FK_ApiKeys_AspNetUsers_UserId", + table: "ApiKeys", + column: "UserId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + if (this.SupportDropForeignKey(migrationBuilder.ActiveProvider)) + { + migrationBuilder.DropForeignKey( + name: "FK_ApiKeys_AspNetUsers_UserId", + table: "ApiKeys"); + } + + migrationBuilder.DropIndex( + name: "IX_ApiKeys_UserId", + table: "ApiKeys"); + if (this.SupportDropColumn(migrationBuilder.ActiveProvider)) + { + migrationBuilder.DropColumn( + name: "Permissions", + table: "ApiKeys"); + + migrationBuilder.DropColumn( + name: "Type", + table: "ApiKeys"); + + migrationBuilder.DropColumn( + name: "UserId", + table: "ApiKeys"); + } + } + } +} diff --git a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs index 64a958e63..c71ba52bd 100644 --- a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -22,14 +22,26 @@ namespace BTCPayServer.Migrations .HasColumnType("TEXT") .HasMaxLength(50); + b.Property("Permissions") + .HasColumnType("TEXT"); + b.Property("StoreId") .HasColumnType("TEXT") .HasMaxLength(50); + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasMaxLength(50); + b.HasKey("Id"); b.HasIndex("StoreId"); + b.HasIndex("UserId"); + b.ToTable("ApiKeys"); }); @@ -842,6 +854,11 @@ namespace BTCPayServer.Migrations .WithMany("APIKeys") .HasForeignKey("StoreId") .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("BTCPayServer.Data.ApplicationUser", "User") + .WithMany("APIKeys") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => diff --git a/BTCPayServer.Data/MigrationsExtensions.cs b/BTCPayServer.Data/MigrationsExtensions.cs index 163238b59..fb354dc85 100644 --- a/BTCPayServer.Data/MigrationsExtensions.cs +++ b/BTCPayServer.Data/MigrationsExtensions.cs @@ -11,7 +11,10 @@ namespace BTCPayServer.Migrations { return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite"; } - + public static bool SupportAddForeignKey(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider) + { + return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite"; + } public static bool SupportDropForeignKey(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider) { return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite"; diff --git a/BTCPayServer.Tests/ApiKeysTests.cs b/BTCPayServer.Tests/ApiKeysTests.cs new file mode 100644 index 000000000..f27760243 --- /dev/null +++ b/BTCPayServer.Tests/ApiKeysTests.cs @@ -0,0 +1,300 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Security.APIKeys; +using BTCPayServer.Tests.Logging; +using BTCPayServer.Views.Manage; +using ExchangeSharp; +using Newtonsoft.Json; +using OpenQA.Selenium; +using Xunit; +using Xunit.Abstractions; + +namespace BTCPayServer.Tests +{ + public class ApiKeysTests + { + public const int TestTimeout = TestUtils.TestTimeout; + + public const string TestApiPath = "api/test/apikey"; + public ApiKeysTests(ITestOutputHelper helper) + { + Logs.Tester = new XUnitLog(helper) {Name = "Tests"}; + Logs.LogProvider = new XUnitLogProvider(helper); + } + + [Fact(Timeout = TestTimeout)] + [Trait("Selenium", "Selenium")] + public async Task CanCreateApiKeys() + { + //there are 2 ways to create api keys: + //as a user through your profile + //as an external application requesting an api key from a user + + using (var s = SeleniumTester.Create()) + { + await s.StartAsync(); + var tester = s.Server; + + var user = tester.NewAccount(); + user.GrantAccess(); + + await user.CreateStoreAsync(); + s.GoToLogin(); + s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password); + s.GoToProfile(ManageNavPages.APIKeys); + s.Driver.FindElement(By.Id("AddApiKey")).Click(); + if (!user.IsAdmin) + { + //not an admin, so this permission should not show + Assert.DoesNotContain("ServerManagementPermission", s.Driver.PageSource); + await user.MakeAdmin(); + s.Logout(); + s.GoToLogin(); + s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password); + s.GoToProfile(ManageNavPages.APIKeys); + s.Driver.FindElement(By.Id("AddApiKey")).Click(); + } + + //server management should show now + s.SetCheckbox(s, "ServerManagementPermission", true); + s.SetCheckbox(s, "StoreManagementPermission", true); + s.Driver.FindElement(By.Id("Generate")).Click(); + var superApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; + + //this api key has access to everything + await TestApiAgainstAccessToken(superApiKey, tester, user, APIKeyConstants.Permissions.ServerManagement, + APIKeyConstants.Permissions.StoreManagement); + + + s.Driver.FindElement(By.Id("AddApiKey")).Click(); + s.SetCheckbox(s, "ServerManagementPermission", true); + s.Driver.FindElement(By.Id("Generate")).Click(); + var serverOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; + await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user, + APIKeyConstants.Permissions.ServerManagement); + + + s.Driver.FindElement(By.Id("AddApiKey")).Click(); + s.SetCheckbox(s, "StoreManagementPermission", true); + s.Driver.FindElement(By.Id("Generate")).Click(); + var allStoreOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; + await TestApiAgainstAccessToken(allStoreOnlyApiKey, tester, user, + APIKeyConstants.Permissions.StoreManagement); + + s.Driver.FindElement(By.Id("AddApiKey")).Click(); + s.Driver.FindElement(By.CssSelector("button[value=change-store-mode]")).Click(); + //there should be a store already by default in the dropdown + var dropdown = s.Driver.FindElement(By.Name("SpecificStores[0]")); + var option = dropdown.FindElement(By.TagName("option")); + var storeId = option.GetAttribute("value"); + option.Click(); + s.Driver.FindElement(By.Id("Generate")).Click(); + var selectiveStoreApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; + await TestApiAgainstAccessToken(selectiveStoreApiKey, tester, user, + APIKeyConstants.Permissions.GetStorePermission(storeId)); + + s.Driver.FindElement(By.Id("AddApiKey")).Click(); + s.Driver.FindElement(By.Id("Generate")).Click(); + var noPermissionsApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; + await TestApiAgainstAccessToken(noPermissionsApiKey, tester, user); + + await Assert.ThrowsAnyAsync(async () => + { + await TestApiAgainstAccessToken("incorrect key", $"{TestApiPath}/me/id", + tester.PayTester.HttpClient); + }); + + + //let's test the authorized screen now + //options for authorize are: + //applicationName + //redirect + //permissions + //strict + //selectiveStores + UriBuilder authorize = new UriBuilder(tester.PayTester.ServerUri); + authorize.Path = "api-keys/authorize"; + + authorize.AppendPayloadToQuery(new Dictionary() + { + {"redirect", "https://local.local/callback"}, + {"applicationName", "kukksappname"}, + {"strict", true}, + {"selectiveStores", false}, + { + "permissions", + new[] + { + APIKeyConstants.Permissions.StoreManagement, + APIKeyConstants.Permissions.ServerManagement + } + }, + }); + var authUrl = authorize.ToString(); + var perms = new[] + { + APIKeyConstants.Permissions.StoreManagement, APIKeyConstants.Permissions.ServerManagement + }; + authUrl = authUrl.Replace("permissions=System.String%5B%5D", + string.Join("&", perms.Select(s1 => $"permissions={s1}"))); + s.Driver.Navigate().GoToUrl(authUrl); + s.Driver.PageSource.Contains("kukksappname"); + Assert.NotNull(s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("readonly")); + Assert.True(s.Driver.FindElement(By.Id("StoreManagementPermission")).Selected); + Assert.NotNull(s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("readonly")); + Assert.True(s.Driver.FindElement(By.Id("ServerManagementPermission")).Selected); + Assert.DoesNotContain("change-store-mode", s.Driver.PageSource); + s.Driver.FindElement(By.Id("consent-yes")).Click(); + var url = s.Driver.Url; + IEnumerable> results = url.Split("?").Last().Split("&") + .Select(s1 => new KeyValuePair(s1.Split("=")[0], s1.Split("=")[1])); + + var apiKeyRepo = s.Server.PayTester.GetService(); + + await TestApiAgainstAccessToken(results.Single(pair => pair.Key == "key").Value, tester, user, + (await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).GetPermissions()); + + authorize = new UriBuilder(tester.PayTester.ServerUri); + authorize.Path = "api-keys/authorize"; + authorize.AppendPayloadToQuery(new Dictionary() + { + {"strict", false}, + {"selectiveStores", true}, + { + "permissions", + new[] + { + APIKeyConstants.Permissions.StoreManagement, + APIKeyConstants.Permissions.ServerManagement + } + } + }); + authUrl = authorize.ToString(); + perms = new[] + { + APIKeyConstants.Permissions.StoreManagement, APIKeyConstants.Permissions.ServerManagement + }; + authUrl = authUrl.Replace("permissions=System.String%5B%5D", + string.Join("&", perms.Select(s1 => $"permissions={s1}"))); + s.Driver.Navigate().GoToUrl(authUrl); + Assert.DoesNotContain("kukksappname", s.Driver.PageSource); + + Assert.Null(s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("readonly")); + Assert.True(s.Driver.FindElement(By.Id("StoreManagementPermission")).Selected); + Assert.Null(s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("readonly")); + Assert.True(s.Driver.FindElement(By.Id("ServerManagementPermission")).Selected); + + s.SetCheckbox(s, "ServerManagementPermission", false); + Assert.Contains("change-store-mode", s.Driver.PageSource); + s.Driver.FindElement(By.Id("consent-yes")).Click(); + url = s.Driver.Url; + results = url.Split("?").Last().Split("&") + .Select(s1 => new KeyValuePair(s1.Split("=")[0], s1.Split("=")[1])); + + await TestApiAgainstAccessToken(results.Single(pair => pair.Key == "key").Value, tester, user, + (await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).GetPermissions()); + + } + } + + async Task TestApiAgainstAccessToken(string accessToken, ServerTester tester, TestAccount testAccount, + params string[] permissions) + { + var resultUser = + await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/id", + tester.PayTester.HttpClient); + Assert.Equal(testAccount.UserId, resultUser); + + //create a second user to see if any of its data gets messed upin our results. + var secondUser = tester.NewAccount(); + secondUser.GrantAccess(); + + var selectiveStorePermissions = APIKeyConstants.Permissions.ExtractStorePermissionsIds(permissions); + if (permissions.Contains(APIKeyConstants.Permissions.StoreManagement) || selectiveStorePermissions.Any()) + { + var resultStores = + await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/stores", + tester.PayTester.HttpClient); + + foreach (string selectiveStorePermission in selectiveStorePermissions) + { + Assert.True(await TestApiAgainstAccessToken(accessToken, + $"{TestApiPath}/me/stores/{selectiveStorePermission}/can-edit", + tester.PayTester.HttpClient)); + + Assert.Contains(resultStores, + data => data.Id.Equals(selectiveStorePermission, StringComparison.InvariantCultureIgnoreCase)); + } + + if (permissions.Contains(APIKeyConstants.Permissions.StoreManagement)) + { + Assert.True(await TestApiAgainstAccessToken(accessToken, + $"{TestApiPath}/me/stores/actions", + tester.PayTester.HttpClient)); + + Assert.True(await TestApiAgainstAccessToken(accessToken, + $"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit", + tester.PayTester.HttpClient)); + Assert.Contains(resultStores, + data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase)); + } + else + { + await Assert.ThrowsAnyAsync(async () => + { + await TestApiAgainstAccessToken(accessToken, + $"{TestApiPath}/me/stores/actions", + tester.PayTester.HttpClient); + }); + } + + Assert.DoesNotContain(resultStores, + data => data.Id.Equals(secondUser.StoreId, StringComparison.InvariantCultureIgnoreCase)); + } + else + { + await Assert.ThrowsAnyAsync(async () => + { + await TestApiAgainstAccessToken(accessToken, + $"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit", + tester.PayTester.HttpClient); + }); + } + + await Assert.ThrowsAnyAsync(async () => + { + await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/stores/{secondUser.StoreId}/can-edit", + tester.PayTester.HttpClient); + }); + + if (permissions.Contains(APIKeyConstants.Permissions.ServerManagement)) + { + Assert.True(await TestApiAgainstAccessToken(accessToken, + $"{TestApiPath}/me/is-admin", + tester.PayTester.HttpClient)); + } + } + + public async Task TestApiAgainstAccessToken(string apikey, string url, HttpClient client) + { + var httpRequest = new HttpRequestMessage(HttpMethod.Get, + new Uri(client.BaseAddress, url)); + httpRequest.Headers.Authorization = new AuthenticationHeaderValue("token", apikey); + 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(rawJson); + } + } +} diff --git a/BTCPayServer.Tests/AuthenticationTests.cs b/BTCPayServer.Tests/AuthenticationTests.cs index 47cb10f85..805db57f8 100644 --- a/BTCPayServer.Tests/AuthenticationTests.cs +++ b/BTCPayServer.Tests/AuthenticationTests.cs @@ -21,6 +21,7 @@ namespace BTCPayServer.Tests { public class AuthenticationTests { + public const string TestApiPath = "api/test/openid"; public const int TestTimeout = TestUtils.TestTimeout; public AuthenticationTests(ITestOutputHelper helper) { @@ -130,12 +131,12 @@ namespace BTCPayServer.Tests await TestApiAgainstAccessToken(results["access_token"], tester, user); var stores = await TestApiAgainstAccessToken(results["access_token"], - $"api/test/me/stores", + $"{TestApiPath}/me/stores", tester.PayTester.HttpClient); Assert.NotEmpty(stores); Assert.True(await TestApiAgainstAccessToken(results["access_token"], - $"api/test/me/stores/{stores[0].Id}/can-edit", + $"{TestApiPath}/me/stores/{stores[0].Id}/can-edit", tester.PayTester.HttpClient)); //we dont ask for consent after acquiring it the first time for the same scopes. @@ -166,13 +167,13 @@ namespace BTCPayServer.Tests await Assert.ThrowsAnyAsync(async () => { await TestApiAgainstAccessToken(results["access_token"], - $"api/test/me/stores", + $"{TestApiPath}/me/stores", tester.PayTester.HttpClient); }); await Assert.ThrowsAnyAsync(async () => { await TestApiAgainstAccessToken(results["access_token"], - $"api/test/me/stores/{stores[0].Id}/can-edit", + $"{TestApiPath}/me/stores/{stores[0].Id}/can-edit", tester.PayTester.HttpClient); }); } @@ -377,7 +378,7 @@ namespace BTCPayServer.Tests async Task TestApiAgainstAccessToken(string accessToken, ServerTester tester, TestAccount testAccount) { var resultUser = - await TestApiAgainstAccessToken(accessToken, "api/test/me/id", + await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/id", tester.PayTester.HttpClient); Assert.Equal(testAccount.UserId, resultUser); @@ -385,7 +386,7 @@ namespace BTCPayServer.Tests secondUser.GrantAccess(); var resultStores = - await TestApiAgainstAccessToken(accessToken, "api/test/me/stores", + await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/stores", tester.PayTester.HttpClient); Assert.Contains(resultStores, data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase)); @@ -393,16 +394,16 @@ namespace BTCPayServer.Tests data => data.Id.Equals(secondUser.StoreId, StringComparison.InvariantCultureIgnoreCase)); Assert.True(await TestApiAgainstAccessToken(accessToken, - $"api/test/me/stores/{testAccount.StoreId}/can-edit", + $"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit", tester.PayTester.HttpClient)); Assert.True(await TestApiAgainstAccessToken(accessToken, - $"api/test/me/is-admin", + $"{TestApiPath}/me/is-admin", tester.PayTester.HttpClient)); await Assert.ThrowsAnyAsync(async () => { - await TestApiAgainstAccessToken(accessToken, $"api/test/me/stores/{secondUser.StoreId}/can-edit", + await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/stores/{secondUser.StoreId}/can-edit", tester.PayTester.HttpClient); }); } diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index 46c2d8767..aba5eec54 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -19,6 +19,7 @@ using System.Threading.Tasks; using BTCPayServer.Lightning; using BTCPayServer.Lightning.CLightning; using BTCPayServer.Models; +using BTCPayServer.Views.Manage; using BTCPayServer.Views.Stores; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -71,19 +72,20 @@ namespace BTCPayServer.Tests Driver.AssertNoError(); } - internal void AssertHappyMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success) + internal IWebElement AssertHappyMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success) { using var cts = new CancellationTokenSource(20_000); while (!cts.IsCancellationRequested) { - var success = Driver.FindElements(By.ClassName($"alert-{StatusMessageModel.ToString(severity)}")).Any(el => el.Displayed); - if (success) - return; + var result = Driver.FindElements(By.ClassName($"alert-{StatusMessageModel.ToString(severity)}")).Where(el => el.Displayed); + if (result.Any()) + return result.First(); Thread.Sleep(100); } Logs.Tester.LogInformation(this.Driver.PageSource); Assert.True(false, $"Should have shown {severity} message"); - } + return null; + } public static readonly TimeSpan ImplicitWait = TimeSpan.FromSeconds(10); public string Link(string relativeLink) @@ -271,6 +273,20 @@ namespace BTCPayServer.Tests { Driver.FindElement(By.Id("Invoices")).Click(); } + + public void GoToProfile(ManageNavPages navPages = ManageNavPages.Index) + { + Driver.FindElement(By.Id("MySettings")).Click(); + if (navPages != ManageNavPages.Index) + { + Driver.FindElement(By.Id(navPages.ToString())).Click(); + } + } + + public void GoToLogin() + { + Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, "Account/Login")); + } public void GoToCreateInvoicePage() { diff --git a/BTCPayServer/Controllers/ManageController.APIKeys.cs b/BTCPayServer/Controllers/ManageController.APIKeys.cs new file mode 100644 index 000000000..26ff2aeec --- /dev/null +++ b/BTCPayServer/Controllers/ManageController.APIKeys.cs @@ -0,0 +1,295 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Models; +using BTCPayServer.Security; +using BTCPayServer.Security.APIKeys; +using ExchangeSharp; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +namespace BTCPayServer.Controllers +{ + public partial class ManageController + { + [HttpGet] + public async Task APIKeys() + { + return View(new ApiKeysViewModel() + { + ApiKeyDatas = await _apiKeyRepository.GetKeys(new APIKeyRepository.APIKeyQuery() + { + UserId = new[] {_userManager.GetUserId(User)} + }) + }); + } + + [HttpGet] + public async Task RemoveAPIKey(string id) + { + await _apiKeyRepository.Remove(id, _userManager.GetUserId(User)); + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Success, + Message = "API Key removed" + }); + return RedirectToAction("APIKeys"); + } + + [HttpGet] + public async Task AddApiKey() + { + if (!_btcPayServerEnvironment.IsSecure) + { + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Error, + Message = "Cannot generate api keys while not on https or tor" + }); + return RedirectToAction("APIKeys"); + } + + return View("AddApiKey", await SetViewModelValues(new AddApiKeyViewModel())); + } + + [HttpGet("~/api-keys/authorize")] + public async Task AuthorizeAPIKey( string[] permissions, string applicationName = null, + bool strict = true, bool selectiveStores = false) + { + if (!_btcPayServerEnvironment.IsSecure) + { + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Error, + Message = "Cannot generate api keys while not on https or tor" + }); + return RedirectToAction("APIKeys"); + } + + permissions ??= Array.Empty(); + + var vm = await SetViewModelValues(new AuthorizeApiKeysViewModel() + { + ServerManagementPermission = permissions.Contains(APIKeyConstants.Permissions.ServerManagement), + StoreManagementPermission = permissions.Contains(APIKeyConstants.Permissions.StoreManagement), + PermissionsFormatted = permissions, + ApplicationName = applicationName, + SelectiveStores = selectiveStores, + Strict = strict, + }); + + vm.ServerManagementPermission = vm.ServerManagementPermission && vm.IsServerAdmin; + return View(vm); + } + + [HttpPost("~/api-keys/authorize")] + public async Task AuthorizeAPIKey([FromForm] AuthorizeApiKeysViewModel viewModel) + { + await SetViewModelValues(viewModel); + var ar = HandleCommands(viewModel); + + if (ar != null) + { + return ar; + } + + + if (viewModel.PermissionsFormatted.Contains(APIKeyConstants.Permissions.ServerManagement)) + { + if (!viewModel.IsServerAdmin && viewModel.ServerManagementPermission) + { + viewModel.ServerManagementPermission = false; + } + + if (!viewModel.ServerManagementPermission && viewModel.Strict) + { + ModelState.AddModelError(nameof(viewModel.ServerManagementPermission), + "This permission is required for this application."); + } + } + + if (viewModel.PermissionsFormatted.Contains(APIKeyConstants.Permissions.StoreManagement)) + { + if (!viewModel.SelectiveStores && + viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific) + { + viewModel.StoreMode = AddApiKeyViewModel.ApiKeyStoreMode.AllStores; + ModelState.AddModelError(nameof(viewModel.StoreManagementPermission), + "This application does not allow selective store permissions."); + } + + if (!viewModel.StoreManagementPermission && !viewModel.SpecificStores.Any() && viewModel.Strict) + { + ModelState.AddModelError(nameof(viewModel.StoreManagementPermission), + "This permission is required for this application."); + } + } + + if (!ModelState.IsValid) + { + return View(viewModel); + } + + switch (viewModel.Command.ToLowerInvariant()) + { + case "no": + return RedirectToAction("APIKeys"); + case "yes": + var key = await CreateKey(viewModel); + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Success, + Html = $"API key generated! {key.Id}" + }); + return RedirectToAction("APIKeys", new { key = key.Id}); + default: return View(viewModel); + } + } + + [HttpPost] + public async Task AddApiKey(AddApiKeyViewModel viewModel) + { + await SetViewModelValues(viewModel); + + var ar = HandleCommands(viewModel); + + if (ar != null) + { + return ar; + } + + if (!ModelState.IsValid) + { + return View(viewModel); + } + + var key = await CreateKey(viewModel); + + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Success, + Html = $"API key generated! {key.Id}" + }); + return RedirectToAction("APIKeys"); + } + private IActionResult HandleCommands(AddApiKeyViewModel viewModel) + { + switch (viewModel.Command) + { + case "change-store-mode": + viewModel.StoreMode = viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific + ? AddApiKeyViewModel.ApiKeyStoreMode.AllStores + : AddApiKeyViewModel.ApiKeyStoreMode.Specific; + + if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific && + !viewModel.SpecificStores.Any() && viewModel.Stores.Any()) + { + viewModel.SpecificStores.Add(null); + } + return View(viewModel); + case "add-store": + viewModel.SpecificStores.Add(null); + return View(viewModel); + + case string x when x.StartsWith("remove-store", StringComparison.InvariantCultureIgnoreCase): + { + ModelState.Clear(); + var index = int.Parse( + viewModel.Command.Substring( + viewModel.Command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1), + CultureInfo.InvariantCulture); + viewModel.SpecificStores.RemoveAt(index); + return View(viewModel); + } + } + + return null; + } + + private async Task CreateKey(AddApiKeyViewModel viewModel) + { + var key = new APIKeyData() + { + Id = Guid.NewGuid().ToString(), Type = APIKeyType.Permanent, UserId = _userManager.GetUserId(User) + }; + key.SetPermissions(GetPermissionsFromViewModel(viewModel)); + await _apiKeyRepository.CreateKey(key); + return key; + } + + private IEnumerable GetPermissionsFromViewModel(AddApiKeyViewModel viewModel) + { + var permissions = new List(); + + if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific) + { + permissions.AddRange(viewModel.SpecificStores.Select(APIKeyConstants.Permissions.GetStorePermission)); + } + else if (viewModel.StoreManagementPermission) + { + permissions.Add(APIKeyConstants.Permissions.StoreManagement); + } + + if (viewModel.IsServerAdmin && viewModel.ServerManagementPermission) + { + permissions.Add(APIKeyConstants.Permissions.ServerManagement); + } + + return permissions; + } + + private async Task SetViewModelValues(T viewModel) where T : AddApiKeyViewModel + { + viewModel.Stores = await _StoreRepository.GetStoresByUserId(_userManager.GetUserId(User)); + viewModel.IsServerAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings.Key)).Succeeded; + return viewModel; + } + + public class AddApiKeyViewModel + { + public StoreData[] Stores { get; set; } + public ApiKeyStoreMode StoreMode { get; set; } + public List SpecificStores { get; set; } = new List(); + public bool IsServerAdmin { get; set; } + public bool ServerManagementPermission { get; set; } + public bool StoreManagementPermission { get; set; } + public string Command { get; set; } + + public enum ApiKeyStoreMode + { + AllStores, + Specific + } + } + + public class AuthorizeApiKeysViewModel : AddApiKeyViewModel + { + public string ApplicationName { get; set; } + public bool Strict { get; set; } + public bool SelectiveStores { get; set; } + public string Permissions { get; set; } + + public string[] PermissionsFormatted + { + get + { + return Permissions?.Split(";", StringSplitOptions.RemoveEmptyEntries); + } + set + { + Permissions = string.Join(';', value ?? Array.Empty()); + } + } + } + + + public class ApiKeysViewModel + { + public List ApiKeyDatas { get; set; } + } + } +} diff --git a/BTCPayServer/Controllers/ManageController.cs b/BTCPayServer/Controllers/ManageController.cs index 46087772d..357f96e8e 100644 --- a/BTCPayServer/Controllers/ManageController.cs +++ b/BTCPayServer/Controllers/ManageController.cs @@ -19,6 +19,8 @@ using System.Globalization; using BTCPayServer.Security; using BTCPayServer.U2F; using BTCPayServer.Data; +using BTCPayServer.Security.APIKeys; + namespace BTCPayServer.Controllers { @@ -34,6 +36,8 @@ namespace BTCPayServer.Controllers IWebHostEnvironment _Env; public U2FService _u2FService; private readonly BTCPayServerEnvironment _btcPayServerEnvironment; + private readonly APIKeyRepository _apiKeyRepository; + private readonly IAuthorizationService _authorizationService; StoreRepository _StoreRepository; @@ -48,7 +52,10 @@ namespace BTCPayServer.Controllers StoreRepository storeRepository, IWebHostEnvironment env, U2FService u2FService, - BTCPayServerEnvironment btcPayServerEnvironment) + BTCPayServerEnvironment btcPayServerEnvironment, + APIKeyRepository apiKeyRepository, + IAuthorizationService authorizationService + ) { _userManager = userManager; _signInManager = signInManager; @@ -58,6 +65,8 @@ namespace BTCPayServer.Controllers _Env = env; _u2FService = u2FService; _btcPayServerEnvironment = btcPayServerEnvironment; + _apiKeyRepository = apiKeyRepository; + _authorizationService = authorizationService; _StoreRepository = storeRepository; } diff --git a/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs b/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs new file mode 100644 index 000000000..d47a732dd --- /dev/null +++ b/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs @@ -0,0 +1,73 @@ +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Security; +using BTCPayServer.Security.APIKeys; +using BTCPayServer.Services.Stores; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Controllers.RestApi +{ + /// + /// this controller serves as a testing endpoint for our api key unit tests + /// + [Route("api/test/apikey")] + [ApiController] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + public class TestApiKeyController : ControllerBase + { + private readonly UserManager _userManager; + private readonly StoreRepository _storeRepository; + + public TestApiKeyController(UserManager userManager, StoreRepository storeRepository) + { + _userManager = userManager; + _storeRepository = storeRepository; + } + + [HttpGet("me/id")] + public string GetCurrentUserId() + { + return _userManager.GetUserId(User); + } + + [HttpGet("me")] + public async Task GetCurrentUser() + { + return await _userManager.GetUserAsync(User); + } + + [HttpGet("me/is-admin")] + [Authorize(Policy = Policies.CanModifyServerSettings.Key, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + public bool AmIAnAdmin() + { + return true; + } + + [HttpGet("me/stores")] + [Authorize(Policy = Policies.CanListStoreSettings.Key, + AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + public async Task GetCurrentUserStores() + { + return await User.GetStores(_userManager, _storeRepository); + } + + [HttpGet("me/stores/actions")] + [Authorize(Policy = Policies.CanModifyStoreSettings.Key, + AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + public bool CanDoNonImplicitStoreActions() + { + return true; + } + + + [HttpGet("me/stores/{storeId}/can-edit")] + [Authorize(Policy = Policies.CanModifyStoreSettings.Key, + AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + public bool CanEdit(string storeId) + { + return true; + } + } +} diff --git a/BTCPayServer/Controllers/RestApi/TestController.cs b/BTCPayServer/Controllers/RestApi/TestOpenIdController.cs similarity index 90% rename from BTCPayServer/Controllers/RestApi/TestController.cs rename to BTCPayServer/Controllers/RestApi/TestOpenIdController.cs index 4ab4d7dbe..0e5623d29 100644 --- a/BTCPayServer/Controllers/RestApi/TestController.cs +++ b/BTCPayServer/Controllers/RestApi/TestOpenIdController.cs @@ -13,15 +13,15 @@ namespace BTCPayServer.Controllers.RestApi /// /// this controller serves as a testing endpoint for our OpenId unit tests /// - [Route("api/[controller]")] + [Route("api/test/openid")] [ApiController] [Authorize(AuthenticationSchemes = AuthenticationSchemes.OpenId)] - public class TestController : ControllerBase + public class TestOpenIdController : ControllerBase { private readonly UserManager _userManager; private readonly StoreRepository _storeRepository; - public TestController(UserManager userManager, StoreRepository storeRepository) + public TestOpenIdController(UserManager userManager, StoreRepository storeRepository) { _userManager = userManager; _storeRepository = storeRepository; @@ -54,7 +54,6 @@ namespace BTCPayServer.Controllers.RestApi return await _storeRepository.GetStoresByUserId(_userManager.GetUserId(User)); } - [HttpGet("me/stores/{storeId}/can-edit")] [Authorize(Policy = Policies.CanModifyStoreSettings.Key, AuthenticationSchemes = AuthenticationSchemes.OpenId)] diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index be19bd8fe..a9064f044 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -32,6 +32,7 @@ using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Changelly; using BTCPayServer.Payments.Lightning; using BTCPayServer.Security; +using BTCPayServer.Security.APIKeys; using BTCPayServer.Services.PaymentRequests; using Microsoft.AspNetCore.Mvc.ModelBinding; using NBXplorer.DerivationStrategy; @@ -241,11 +242,11 @@ namespace BTCPayServer.Hosting services.AddTransient(); // Add application services. services.AddSingleton(); - // bundling - - services.AddBtcPayServerAuthenticationSchemes(configuration); + + services.AddAPIKeyAuthentication(); + services.AddBtcPayServerAuthenticationSchemes(); services.AddAuthorization(o => o.AddBTCPayPolicies()); - + // bundling services.AddSingleton(); services.AddTransient(provider => { @@ -292,12 +293,12 @@ namespace BTCPayServer.Hosting return services; } private const long MAX_DEBUG_LOG_FILE_SIZE = 2000000; // If debug log is in use roll it every N MB. - private static void AddBtcPayServerAuthenticationSchemes(this IServiceCollection services, - IConfiguration configuration) + private static void AddBtcPayServerAuthenticationSchemes(this IServiceCollection services) { services.AddAuthentication() .AddCookie() - .AddBitpayAuthentication(); + .AddBitpayAuthentication() + .AddAPIKeyAuthentication(); } public static IApplicationBuilder UsePayServer(this IApplicationBuilder app) diff --git a/BTCPayServer/Security/APIKeys/APIKeyAuthenticationHandler.cs b/BTCPayServer/Security/APIKeys/APIKeyAuthenticationHandler.cs new file mode 100644 index 000000000..056b48180 --- /dev/null +++ b/BTCPayServer/Security/APIKeys/APIKeyAuthenticationHandler.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Security.Bitpay; +using BTCPayServer.Services.Stores; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace BTCPayServer.Security.APIKeys +{ + public class APIKeyAuthenticationHandler : AuthenticationHandler + { + private readonly APIKeyRepository _apiKeyRepository; + private readonly IOptionsMonitor _identityOptions; + + public APIKeyAuthenticationHandler( + APIKeyRepository apiKeyRepository, + IOptionsMonitor identityOptions, + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock) : base(options, logger, encoder, clock) + { + _apiKeyRepository = apiKeyRepository; + _identityOptions = identityOptions; + } + + protected override async Task HandleAuthenticateAsync() + { + if (!Context.Request.HttpContext.GetAPIKey(out var apiKey) || string.IsNullOrEmpty(apiKey)) + return AuthenticateResult.NoResult(); + + var key = await _apiKeyRepository.GetKey(apiKey); + + if (key == null) + { + return AuthenticateResult.Fail("ApiKey authentication failed"); + } + + List claims = new List(); + + claims.Add(new Claim(_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, key.UserId)); + claims.AddRange(key.GetPermissions() + .Select(permission => new Claim(APIKeyConstants.ClaimTypes.Permissions, permission))); + + return AuthenticateResult.Success(new AuthenticationTicket( + new ClaimsPrincipal(new ClaimsIdentity(claims, APIKeyConstants.AuthenticationType)), APIKeyConstants.AuthenticationType)); + } + } +} diff --git a/BTCPayServer/Security/APIKeys/APIKeyAuthenticationOptions.cs b/BTCPayServer/Security/APIKeys/APIKeyAuthenticationOptions.cs new file mode 100644 index 000000000..d03e796cd --- /dev/null +++ b/BTCPayServer/Security/APIKeys/APIKeyAuthenticationOptions.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Authentication; + +namespace BTCPayServer.Security.Bitpay +{ + public class APIKeyAuthenticationOptions : AuthenticationSchemeOptions + { + } +} diff --git a/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs b/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs new file mode 100644 index 000000000..0ed687374 --- /dev/null +++ b/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs @@ -0,0 +1,84 @@ +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Services.Stores; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; + +namespace BTCPayServer.Security.APIKeys +{ + public class APIKeyAuthorizationHandler : AuthorizationHandler + + { + private readonly HttpContext _HttpContext; + private readonly UserManager _userManager; + private readonly StoreRepository _storeRepository; + + public APIKeyAuthorizationHandler(IHttpContextAccessor httpContextAccessor, + UserManager userManager, + StoreRepository storeRepository) + { + _HttpContext = httpContextAccessor.HttpContext; + _userManager = userManager; + _storeRepository = storeRepository; + } + + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, + PolicyRequirement requirement) + { + if (context.User.Identity.AuthenticationType != APIKeyConstants.AuthenticationType) + return; + + bool success = false; + switch (requirement.Policy) + { + case Policies.CanListStoreSettings.Key: + var selectiveStorePermissions = + APIKeyConstants.Permissions.ExtractStorePermissionsIds(context.GetPermissions()); + success = context.HasPermissions(APIKeyConstants.Permissions.StoreManagement) || + selectiveStorePermissions.Any(); + break; + case Policies.CanModifyStoreSettings.Key: + string storeId = _HttpContext.GetImplicitStoreId(); + if (!context.HasPermissions(APIKeyConstants.Permissions.StoreManagement) && + !context.HasPermissions(APIKeyConstants.Permissions.GetStorePermission(storeId))) + break; + + if (storeId == null) + { + success = true; + } + else + { + var userid = _userManager.GetUserId(context.User); + if (string.IsNullOrEmpty(userid)) + break; + var store = await _storeRepository.FindStore((string)storeId, userid); + if (store == null) + break; + success = true; + _HttpContext.SetStoreData(store); + } + + break; + case Policies.CanModifyServerSettings.Key: + if (!context.HasPermissions(APIKeyConstants.Permissions.ServerManagement)) + break; + // For this authorization, we stil check in database because it is super sensitive. + var user = await _userManager.GetUserAsync(context.User); + if (user == null) + break; + if (!await _userManager.IsInRoleAsync(user, Roles.ServerAdmin)) + break; + success = true; + break; + } + + if (success) + { + context.Succeed(requirement); + } + } + } +} diff --git a/BTCPayServer/Security/APIKeys/APIKeyConstants.cs b/BTCPayServer/Security/APIKeys/APIKeyConstants.cs new file mode 100644 index 000000000..427de68e5 --- /dev/null +++ b/BTCPayServer/Security/APIKeys/APIKeyConstants.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace BTCPayServer.Security.APIKeys +{ + public static class APIKeyConstants + { + public const string AuthenticationType = "APIKey"; + + public static class ClaimTypes + { + public const string Permissions = nameof(APIKeys) + "." + nameof(Permissions); + } + + public static class Permissions + { + public const string ServerManagement = nameof(ServerManagement); + public const string StoreManagement = nameof(StoreManagement); + + public static readonly Dictionary PermissionDescriptions = new Dictionary() + { + {StoreManagement, ("Manage your stores", "The app will be able to create, modify and delete all your stores.")}, + {$"{nameof(StoreManagement)}:", ("Manage selected stores", "The app will be able to modify and delete selected stores.")}, + {ServerManagement, ("Manage your server", "The app will have total control on your server")}, + }; + + public static string GetStorePermission(string storeId) => $"{nameof(StoreManagement)}:{storeId}"; + + public static IEnumerable ExtractStorePermissionsIds(IEnumerable permissions) => permissions + .Where(s => s.StartsWith($"{nameof(StoreManagement)}:", StringComparison.InvariantCulture)) + .Select(s => s.Split(":")[1]); + } + } +} diff --git a/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs b/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs new file mode 100644 index 000000000..17f696f0d --- /dev/null +++ b/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs @@ -0,0 +1,75 @@ +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Security.Bitpay; +using BTCPayServer.Services.Stores; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; + +namespace BTCPayServer.Security.APIKeys +{ + public static class APIKeyExtensions + { + public static bool GetAPIKey(this HttpContext httpContext, out StringValues apiKey) + { + if (httpContext.Request.Headers.TryGetValue("Authorization", out var value) && + value.ToString().StartsWith("token ", StringComparison.InvariantCultureIgnoreCase)) + { + apiKey = value.ToString().Substring("token ".Length); + return true; + } + + return false; + } + + public static Task GetStores(this ClaimsPrincipal claimsPrincipal, + UserManager userManager, StoreRepository storeRepository) + { + var permissions = + claimsPrincipal.Claims.Where(claim => claim.Type == APIKeyConstants.ClaimTypes.Permissions) + .Select(claim => claim.Value).ToList(); + + if (permissions.Contains(APIKeyConstants.Permissions.StoreManagement)) + { + return storeRepository.GetStoresByUserId(userManager.GetUserId(claimsPrincipal)); + } + + var storeIds = APIKeyConstants.Permissions.ExtractStorePermissionsIds(permissions); + return storeRepository.GetStoresByUserId(userManager.GetUserId(claimsPrincipal), storeIds); + } + + public static AuthenticationBuilder AddAPIKeyAuthentication(this AuthenticationBuilder builder) + { + builder.AddScheme(AuthenticationSchemes.ApiKey, + o => { }); + return builder; + } + + public static IServiceCollection AddAPIKeyAuthentication(this IServiceCollection serviceCollection) + { + serviceCollection.AddSingleton(); + serviceCollection.AddScoped(); + return serviceCollection; + } + + public static string[] GetPermissions(this AuthorizationHandlerContext context) + { + return context.User.Claims.Where(c => + c.Type.Equals(APIKeyConstants.ClaimTypes.Permissions, StringComparison.InvariantCultureIgnoreCase)) + .Select(claim => claim.Value).ToArray(); + } + + public static bool HasPermissions(this AuthorizationHandlerContext context, params string[] scopes) + { + return scopes.All(s => context.User.HasClaim(c => + c.Type.Equals(APIKeyConstants.ClaimTypes.Permissions, StringComparison.InvariantCultureIgnoreCase) && + c.Value.Split(' ').Contains(s))); + } + } +} diff --git a/BTCPayServer/Security/APIKeys/APIKeyRepository.cs b/BTCPayServer/Security/APIKeys/APIKeyRepository.cs new file mode 100644 index 000000000..c30fd7a8c --- /dev/null +++ b/BTCPayServer/Security/APIKeys/APIKeyRepository.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Data; +using Microsoft.EntityFrameworkCore; + +namespace BTCPayServer.Security.APIKeys +{ + public class APIKeyRepository + { + private readonly ApplicationDbContextFactory _applicationDbContextFactory; + + public APIKeyRepository(ApplicationDbContextFactory applicationDbContextFactory) + { + _applicationDbContextFactory = applicationDbContextFactory; + } + + public async Task GetKey(string apiKey) + { + using (var context = _applicationDbContextFactory.CreateContext()) + { + return await EntityFrameworkQueryableExtensions.SingleOrDefaultAsync(context.ApiKeys, + data => data.Id == apiKey && data.Type != APIKeyType.Legacy); + } + } + + public async Task> GetKeys(APIKeyQuery query) + { + using (var context = _applicationDbContextFactory.CreateContext()) + { + var queryable = context.ApiKeys.AsQueryable(); + if (query?.UserId != null && query.UserId.Any()) + { + queryable = queryable.Where(data => query.UserId.Contains(data.UserId)); + } + + return await queryable.ToListAsync(); + } + } + + public async Task CreateKey(APIKeyData key) + { + if (key.Type == APIKeyType.Legacy || !string.IsNullOrEmpty(key.StoreId) || string.IsNullOrEmpty(key.UserId)) + { + throw new InvalidOperationException("cannot save a bitpay legacy api key with this repository"); + } + + using (var context = _applicationDbContextFactory.CreateContext()) + { + await context.ApiKeys.AddAsync(key); + await context.SaveChangesAsync(); + } + } + + public async Task Remove(string id, string getUserId) + { + using (var context = _applicationDbContextFactory.CreateContext()) + { + var key = await EntityFrameworkQueryableExtensions.SingleOrDefaultAsync(context.ApiKeys, + data => data.Id == id && data.UserId == getUserId); + context.ApiKeys.Remove(key); + await context.SaveChangesAsync(); + } + } + + public class APIKeyQuery + { + public string[] UserId { get; set; } + } + } +} diff --git a/BTCPayServer/Security/AuthenticationSchemes.cs b/BTCPayServer/Security/AuthenticationSchemes.cs index 0f986f4e2..a3117ca3a 100644 --- a/BTCPayServer/Security/AuthenticationSchemes.cs +++ b/BTCPayServer/Security/AuthenticationSchemes.cs @@ -12,5 +12,6 @@ namespace BTCPayServer.Security public const string Cookie = "Identity.Application"; public const string Bitpay = "Bitpay"; public const string OpenId = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme; + public const string ApiKey = "GreenfieldApiKey"; } } diff --git a/BTCPayServer/Security/Bitpay/TokenRepository.cs b/BTCPayServer/Security/Bitpay/TokenRepository.cs index cb0289a52..b5ac7fa3c 100644 --- a/BTCPayServer/Security/Bitpay/TokenRepository.cs +++ b/BTCPayServer/Security/Bitpay/TokenRepository.cs @@ -66,10 +66,10 @@ namespace BTCPayServer.Security.Bitpay using (var ctx = _Factory.CreateContext()) { - var existing = await ctx.ApiKeys.Where(o => o.StoreId == storeId).FirstOrDefaultAsync(); - if (existing != null) + var existing = await ctx.ApiKeys.Where(o => o.StoreId == storeId && o.Type == APIKeyType.Legacy).ToListAsync(); + if (existing.Any()) { - ctx.ApiKeys.Remove(existing); + ctx.ApiKeys.RemoveRange(existing); } ctx.ApiKeys.Add(new APIKeyData() { Id = new string(generated), StoreId = storeId }); await ctx.SaveChangesAsync().ConfigureAwait(false); @@ -95,7 +95,7 @@ namespace BTCPayServer.Security.Bitpay { using (var ctx = _Factory.CreateContext()) { - return await ctx.ApiKeys.Where(o => o.StoreId == storeId).Select(c => c.Id).ToArrayAsync(); + return await ctx.ApiKeys.Where(o => o.StoreId == storeId && o.Type== APIKeyType.Legacy).Select(c => c.Id).ToArrayAsync(); } } diff --git a/BTCPayServer/Security/Policies.cs b/BTCPayServer/Security/Policies.cs index ad2a711f0..e21c10c95 100644 --- a/BTCPayServer/Security/Policies.cs +++ b/BTCPayServer/Security/Policies.cs @@ -11,6 +11,7 @@ namespace BTCPayServer.Security public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options) { options.AddPolicy(CanModifyStoreSettings.Key); + options.AddPolicy(CanListStoreSettings.Key); options.AddPolicy(CanCreateInvoice.Key); options.AddPolicy(CanGetRates.Key); options.AddPolicy(CanModifyServerSettings.Key); @@ -30,6 +31,10 @@ namespace BTCPayServer.Security { public const string Key = "btcpay.store.canmodifystoresettings"; } + public class CanListStoreSettings + { + public const string Key = "btcpay.store.canliststoresettings"; + } public class CanCreateInvoice { public const string Key = "btcpay.store.cancreateinvoice"; diff --git a/BTCPayServer/Services/Stores/StoreRepository.cs b/BTCPayServer/Services/Stores/StoreRepository.cs index 8f202cc1b..09c1e6cac 100644 --- a/BTCPayServer/Services/Stores/StoreRepository.cs +++ b/BTCPayServer/Services/Stores/StoreRepository.cs @@ -78,12 +78,12 @@ namespace BTCPayServer.Services.Stores } } - public async Task GetStoresByUserId(string userId) + public async Task GetStoresByUserId(string userId, IEnumerable storeIds = null) { using (var ctx = _ContextFactory.CreateContext()) { return (await ctx.UserStore - .Where(u => u.ApplicationUserId == userId) + .Where(u => u.ApplicationUserId == userId && (storeIds == null || storeIds.Contains(u.StoreDataId))) .Select(u => new { u.StoreData, u.Role }) .ToArrayAsync()) .Select(u => diff --git a/BTCPayServer/Views/Manage/APIKeys.cshtml b/BTCPayServer/Views/Manage/APIKeys.cshtml new file mode 100644 index 000000000..23db9ac73 --- /dev/null +++ b/BTCPayServer/Views/Manage/APIKeys.cshtml @@ -0,0 +1,50 @@ +@model BTCPayServer.Controllers.ManageController.ApiKeysViewModel +@{ + ViewData.SetActivePageAndTitle(ManageNavPages.APIKeys, "Manage your API Keys"); +} + + +

API Keys

+ + + + + + + + + + @foreach (var keyData in Model.ApiKeyDatas) + { + + + + + + } + @if (!Model.ApiKeyDatas.Any()) + { + + + + } + + + + +
KeyPermissionsActions
@keyData.Id + @if (string.IsNullOrEmpty(keyData.Permissions)) + { + No permissions + } + else + { + @string.Join(", ", keyData.GetPermissions()) + } + + Remove +
+ No API keys +
+ Generate new key +
diff --git a/BTCPayServer/Views/Manage/AddApiKey.cshtml b/BTCPayServer/Views/Manage/AddApiKey.cshtml new file mode 100644 index 000000000..e992c0a38 --- /dev/null +++ b/BTCPayServer/Views/Manage/AddApiKey.cshtml @@ -0,0 +1,120 @@ +@using BTCPayServer.Controllers +@using BTCPayServer.Security.APIKeys +@model BTCPayServer.Controllers.ManageController.AddApiKeyViewModel + +@{ + ViewData.SetActivePageAndTitle(ManageNavPages.APIKeys, "Add API Key"); + + string GetDescription(string permission) + { + return APIKeyConstants.Permissions.PermissionDescriptions[permission].Description; + } + + string GetTitle(string permission) + { + return APIKeyConstants.Permissions.PermissionDescriptions[permission].Description; + } +} + +

@ViewData["Title"]

+ +

+ Generate a new api key to use BTCPay through its API. +

+
+
+
+ + +
+ @if (Model.IsServerAdmin) + { +
+ + + +

@GetDescription(APIKeyConstants.Permissions.ServerManagement).

+
+ } + @if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores) + { +
+ @Html.CheckBoxFor(model => model.StoreManagementPermission, new Dictionary() {{"class", "form-check-inline"}}) + + + +

@GetDescription(APIKeyConstants.Permissions.StoreManagement).

+ +
+ } + else if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.Specific) + { +
+
  • +
    @GetTitle(APIKeyConstants.Permissions.StoreManagement + ":")
    +

    @GetDescription(APIKeyConstants.Permissions.StoreManagement + ":").

    + +
  • + @if (!Model.Stores.Any()) + { +
  • + You currently have no stores configured. +
  • + } + @for (var index = 0; index < Model.SpecificStores.Count; index++) + { +
    +
    +
    +
    + @if (Model.SpecificStores[index] == null) + { + + } + else + { + var store = Model.Stores.SingleOrDefault(data => data.Id == Model.SpecificStores[index]); + + } + + +
    +
    +
    + + +
    +
    +
    + } + @if (Model.SpecificStores.Count < Model.Stores.Length) + { +
    + +
    + } +
    + } + +
    +
    +
    + +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") + + +} diff --git a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml new file mode 100644 index 000000000..136d480a3 --- /dev/null +++ b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml @@ -0,0 +1,137 @@ +@using BTCPayServer.Controllers +@using BTCPayServer.Security.APIKeys +@model BTCPayServer.Controllers.ManageController.AuthorizeApiKeysViewModel + +@{ + Layout = "_Layout"; + ViewData["Title"] = $"Authorize {(Model.ApplicationName ?? "Application")}"; + + string GetDescription(string permission) + { + return APIKeyConstants.Permissions.PermissionDescriptions[permission].Description; + } + + string GetTitle(string permission) + { + return APIKeyConstants.Permissions.PermissionDescriptions[permission].Description; + } +} + + +
    + + + + +
    +
    +
    +
    +

    Authorization Request

    +
    +

    @(Model.ApplicationName ?? "An application") is requesting access to your account.

    +
    +
    +
    +
    +
    + @if (Model.PermissionsFormatted.Contains(APIKeyConstants.Permissions.ServerManagement) && (Model.IsServerAdmin || Model.Strict)) + { +
    + + + @if (!Model.IsServerAdmin) + { + + The server management permission is being requested but your account is not an administrator + + } + + +

    @GetDescription(APIKeyConstants.Permissions.ServerManagement).

    +
    + } + + @if (Model.PermissionsFormatted.Contains(APIKeyConstants.Permissions.StoreManagement)) + { + @if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores) + { +
    + + + +

    @GetDescription(APIKeyConstants.Permissions.StoreManagement).

    + @if (Model.SelectiveStores) + { + + } +
    + } + else if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.Specific) + { +
    +
  • +
    @GetTitle(APIKeyConstants.Permissions.StoreManagement + ":")
    +

    @GetDescription(APIKeyConstants.Permissions.StoreManagement + ":").

    + +
  • + @if (!Model.Stores.Any()) + { +
  • + You currently have no stores configured. +
  • + } + @for (var index = 0; index < Model.SpecificStores.Count; index++) + { +
    +
    +
    +
    + @if (Model.SpecificStores[index] == null) + { + + } + else + { + var store = Model.Stores.SingleOrDefault(data => data.Id == Model.SpecificStores[index]); + + } + + +
    +
    +
    + + +
    +
    +
    + } + @if (Model.SpecificStores.Count < Model.Stores.Length) + { +
    + +
    + } +
    + } + } +
    +
    +
    +
    +
    + +
    + + +
    +
    +
    +
    +
    diff --git a/BTCPayServer/Views/Manage/ManageNavPages.cs b/BTCPayServer/Views/Manage/ManageNavPages.cs index 6227b33d7..c197c829b 100644 --- a/BTCPayServer/Views/Manage/ManageNavPages.cs +++ b/BTCPayServer/Views/Manage/ManageNavPages.cs @@ -7,6 +7,6 @@ namespace BTCPayServer.Views.Manage { public enum ManageNavPages { - Index, ChangePassword, TwoFactorAuthentication, U2F + Index, ChangePassword, TwoFactorAuthentication, U2F, APIKeys } } diff --git a/BTCPayServer/Views/Manage/_Nav.cshtml b/BTCPayServer/Views/Manage/_Nav.cshtml index 7c27d57cf..8c613b3c6 100644 --- a/BTCPayServer/Views/Manage/_Nav.cshtml +++ b/BTCPayServer/Views/Manage/_Nav.cshtml @@ -1,9 +1,10 @@ @inject SignInManager SignInManager From d16a4334cb4de227fdddcc65ce2c5deb4a8e8205 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 25 Feb 2020 00:10:07 +0900 Subject: [PATCH 214/810] Fix error 500 on services page --- BTCPayServer/Extensions.cs | 4 +++- Build/Version.csproj | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 19e61beec..1d1736423 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -268,7 +268,9 @@ namespace BTCPayServer public static bool IsOnion(this Uri uri) { - return uri?.DnsSafeHost?.EndsWith(".onion", StringComparison.OrdinalIgnoreCase) is true; + if (uri == null || !uri.IsAbsoluteUri) + return false; + return uri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase); } diff --git a/Build/Version.csproj b/Build/Version.csproj index 5c813fd8b..a7ee9036c 100644 --- a/Build/Version.csproj +++ b/Build/Version.csproj @@ -1,5 +1,5 @@ - 1.0.3.160 + 1.0.3.161 From 276a9a95f9d94a2ba1889c8354521f8f0f3ae37b Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Mon, 24 Feb 2020 16:40:04 +0100 Subject: [PATCH 215/810] Remove OpenIddict (#1244) --- BTCPayServer.Data/BTCPayServer.Data.csproj | 1 - .../Data/ApplicationDbContext.cs | 23 - BTCPayServer.Data/Data/ApplicationUser.cs | 4 +- .../Data/BTCPayOpenIdAuthorization.cs | 6 - BTCPayServer.Data/Data/BTCPayOpenIdClient.cs | 10 - BTCPayServer.Data/Data/BTCPayOpenIdToken.cs | 6 - .../20200224134444_Remove_OpenIddict.cs | 169 +++++++ .../ApplicationDbContextModelSnapshot.cs | 219 --------- BTCPayServer.Tests/AuthenticationTests.cs | 428 ------------------ BTCPayServer.Tests/BTCPayServerTester.cs | 3 +- BTCPayServer.Tests/TestAccount.cs | 11 - BTCPayServer/BTCPayServer.csproj | 7 +- .../Controllers/AuthorizationController.cs | 136 ------ .../RestApi/TestOpenIdController.cs | 65 --- .../Extensions/OpenIddictExtensions.cs | 37 -- BTCPayServer/Extensions/RsaKeyExtensions.cs | 97 ---- BTCPayServer/Hosting/BTCPayServerServices.cs | 13 - BTCPayServer/Hosting/Startup.cs | 80 +--- BTCPayServer/Program.cs | 1 - .../Security/AuthenticationSchemes.cs | 10 +- BTCPayServer/Security/OpenId/BTCPayScopes.cs | 19 - .../Security/OpenId/BaseOpenIdGrantHandler.cs | 43 -- .../ClientCredentialsGrantTypeEventHandler.cs | 61 --- .../Security/OpenId/LogoutEventHandler.cs | 34 -- .../Security/OpenId/OpenIdExtensions.cs | 106 ----- .../OpenIdGrantHandlerCheckCanSignIn.cs | 70 --- .../OpenId/PasswordGrantTypeEventHandler.cs | 65 --- .../Security/OpenIdAuthorizationHandler.cs | 83 ---- BTCPayServer/Security/SecurityExtensions.cs | 8 +- .../Views/Authorization/Authorize.cshtml | 57 --- 30 files changed, 182 insertions(+), 1690 deletions(-) delete mode 100644 BTCPayServer.Data/Data/BTCPayOpenIdAuthorization.cs delete mode 100644 BTCPayServer.Data/Data/BTCPayOpenIdClient.cs delete mode 100644 BTCPayServer.Data/Data/BTCPayOpenIdToken.cs create mode 100644 BTCPayServer.Data/Migrations/20200224134444_Remove_OpenIddict.cs delete mode 100644 BTCPayServer.Tests/AuthenticationTests.cs delete mode 100644 BTCPayServer/Controllers/AuthorizationController.cs delete mode 100644 BTCPayServer/Controllers/RestApi/TestOpenIdController.cs delete mode 100644 BTCPayServer/Extensions/OpenIddictExtensions.cs delete mode 100644 BTCPayServer/Extensions/RsaKeyExtensions.cs delete mode 100644 BTCPayServer/Security/OpenId/BTCPayScopes.cs delete mode 100644 BTCPayServer/Security/OpenId/BaseOpenIdGrantHandler.cs delete mode 100644 BTCPayServer/Security/OpenId/ClientCredentialsGrantTypeEventHandler.cs delete mode 100644 BTCPayServer/Security/OpenId/LogoutEventHandler.cs delete mode 100644 BTCPayServer/Security/OpenId/OpenIdExtensions.cs delete mode 100644 BTCPayServer/Security/OpenId/OpenIdGrantHandlerCheckCanSignIn.cs delete mode 100644 BTCPayServer/Security/OpenId/PasswordGrantTypeEventHandler.cs delete mode 100644 BTCPayServer/Security/OpenIdAuthorizationHandler.cs delete mode 100644 BTCPayServer/Views/Authorization/Authorize.cshtml diff --git a/BTCPayServer.Data/BTCPayServer.Data.csproj b/BTCPayServer.Data/BTCPayServer.Data.csproj index 6639da853..a8612e9cf 100644 --- a/BTCPayServer.Data/BTCPayServer.Data.csproj +++ b/BTCPayServer.Data/BTCPayServer.Data.csproj @@ -7,7 +7,6 @@ - diff --git a/BTCPayServer.Data/Data/ApplicationDbContext.cs b/BTCPayServer.Data/Data/ApplicationDbContext.cs index e245b85bc..b00c8e1ba 100644 --- a/BTCPayServer.Data/Data/ApplicationDbContext.cs +++ b/BTCPayServer.Data/Data/ApplicationDbContext.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.EntityFrameworkCore.Infrastructure; -using OpenIddict.EntityFrameworkCore.Models; namespace BTCPayServer.Data { @@ -261,28 +260,6 @@ namespace BTCPayServer.Data .HasOne(o => o.WalletData) .WithMany(w => w.WalletTransactions).OnDelete(DeleteBehavior.Cascade); - builder.UseOpenIddict, BTCPayOpenIdToken, string>(); - - if (Database.IsSqlite() && !_designTime) - { - // SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations - // here: https://docs.microsoft.com/en-us/ef/core/providers/sqlite/limitations#query-limitations - // To work around this, when the Sqlite database provider is used, all model properties of type DateTimeOffset - // use the DateTimeOffsetToBinaryConverter - // Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754 - // This only supports millisecond precision, but should be sufficient for most use cases. - foreach (var entityType in builder.Model.GetEntityTypes()) - { - var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset)); - foreach (var property in properties) - { - builder - .Entity(entityType.Name) - .Property(property.Name) - .HasConversion(new Microsoft.EntityFrameworkCore.Storage.ValueConversion.DateTimeOffsetToBinaryConverter()); - } - } - } } } diff --git a/BTCPayServer.Data/Data/ApplicationUser.cs b/BTCPayServer.Data/Data/ApplicationUser.cs index 1286e147c..6c9c28fbc 100644 --- a/BTCPayServer.Data/Data/ApplicationUser.cs +++ b/BTCPayServer.Data/Data/ApplicationUser.cs @@ -20,9 +20,7 @@ namespace BTCPayServer.Data { get; set; } - - public List OpenIdClients { get; set; } - + public List StoredFiles { get; diff --git a/BTCPayServer.Data/Data/BTCPayOpenIdAuthorization.cs b/BTCPayServer.Data/Data/BTCPayOpenIdAuthorization.cs deleted file mode 100644 index cfc3c3bfc..000000000 --- a/BTCPayServer.Data/Data/BTCPayOpenIdAuthorization.cs +++ /dev/null @@ -1,6 +0,0 @@ -using OpenIddict.EntityFrameworkCore.Models; - -namespace BTCPayServer.Data -{ - public class BTCPayOpenIdAuthorization : OpenIddictAuthorization { } -} diff --git a/BTCPayServer.Data/Data/BTCPayOpenIdClient.cs b/BTCPayServer.Data/Data/BTCPayOpenIdClient.cs deleted file mode 100644 index eb4e94b25..000000000 --- a/BTCPayServer.Data/Data/BTCPayOpenIdClient.cs +++ /dev/null @@ -1,10 +0,0 @@ -using OpenIddict.EntityFrameworkCore.Models; - -namespace BTCPayServer.Data -{ - public class BTCPayOpenIdClient: OpenIddictApplication - { - public string ApplicationUserId { get; set; } - public ApplicationUser ApplicationUser { get; set; } - } -} diff --git a/BTCPayServer.Data/Data/BTCPayOpenIdToken.cs b/BTCPayServer.Data/Data/BTCPayOpenIdToken.cs deleted file mode 100644 index 94951a118..000000000 --- a/BTCPayServer.Data/Data/BTCPayOpenIdToken.cs +++ /dev/null @@ -1,6 +0,0 @@ -using OpenIddict.EntityFrameworkCore.Models; - -namespace BTCPayServer.Data -{ - public class BTCPayOpenIdToken : OpenIddictToken { } -} diff --git a/BTCPayServer.Data/Migrations/20200224134444_Remove_OpenIddict.cs b/BTCPayServer.Data/Migrations/20200224134444_Remove_OpenIddict.cs new file mode 100644 index 000000000..78f323529 --- /dev/null +++ b/BTCPayServer.Data/Migrations/20200224134444_Remove_OpenIddict.cs @@ -0,0 +1,169 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace BTCPayServer.Migrations +{ + public partial class Remove_OpenIddict : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OpenIddictScopes"); + + migrationBuilder.DropTable( + name: "OpenIddictTokens"); + + migrationBuilder.DropTable( + name: "OpenIddictAuthorizations"); + + migrationBuilder.DropTable( + name: "OpenIddictApplications"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null; + migrationBuilder.CreateTable( + name: "OpenIddictApplications", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false, maxLength: maxLength), + ApplicationUserId = table.Column(type: "TEXT", nullable: true, maxLength: maxLength), + ClientId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + ClientSecret = table.Column(type: "TEXT", nullable: true), + ConcurrencyToken = table.Column(type: "TEXT", maxLength: 50, nullable: true), + ConsentType = table.Column(type: "TEXT", nullable: true), + DisplayName = table.Column(type: "TEXT", nullable: true), + Permissions = table.Column(type: "TEXT", nullable: true), + PostLogoutRedirectUris = table.Column(type: "TEXT", nullable: true), + Properties = table.Column(type: "TEXT", nullable: true), + RedirectUris = table.Column(type: "TEXT", nullable: true), + Requirements = table.Column(type: "TEXT", nullable: true), + Type = table.Column(type: "TEXT", maxLength: 25, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictApplications", x => x.Id); + table.ForeignKey( + name: "FK_OpenIddictApplications_AspNetUsers_ApplicationUserId", + column: x => x.ApplicationUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictScopes", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false, maxLength: maxLength), + ConcurrencyToken = table.Column(type: "TEXT", maxLength: 50, nullable: true), + Description = table.Column(type: "TEXT", nullable: true), + DisplayName = table.Column(type: "TEXT", nullable: true), + Name = table.Column(type: "TEXT", maxLength: 200, nullable: false), + Properties = table.Column(type: "TEXT", nullable: true), + Resources = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictScopes", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictAuthorizations", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false, maxLength: maxLength), + ApplicationId = table.Column(type: "TEXT", nullable: true, maxLength: maxLength), + ConcurrencyToken = table.Column(type: "TEXT", maxLength: 50, nullable: true), + Properties = table.Column(type: "TEXT", nullable: true), + Scopes = table.Column(type: "TEXT", nullable: true), + Status = table.Column(type: "TEXT", maxLength: 25, nullable: false), + Subject = table.Column(type: "TEXT", maxLength: 450, nullable: true), + Type = table.Column(type: "TEXT", maxLength: 25, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictAuthorizations", x => x.Id); + table.ForeignKey( + name: "FK_OpenIddictAuthorizations_OpenIddictApplications_ApplicationId", + column: x => x.ApplicationId, + principalTable: "OpenIddictApplications", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictTokens", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false, maxLength: maxLength), + ApplicationId = table.Column(type: "TEXT", nullable: true, maxLength: maxLength), + AuthorizationId = table.Column(type: "TEXT", nullable: true, maxLength: maxLength), + ConcurrencyToken = table.Column(type: "TEXT", maxLength: 50, nullable: true), + CreationDate = table.Column(type: "TEXT", nullable: true), + ExpirationDate = table.Column(type: "TEXT", nullable: true), + Payload = table.Column(type: "TEXT", nullable: true), + Properties = table.Column(type: "TEXT", nullable: true), + ReferenceId = table.Column(type: "TEXT", maxLength: 100, nullable: true), + Status = table.Column(type: "TEXT", maxLength: 25, nullable: false), + Subject = table.Column(type: "TEXT", maxLength: 450, nullable: true), + Type = table.Column(type: "TEXT", maxLength: 25, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictTokens", x => x.Id); + table.ForeignKey( + name: "FK_OpenIddictTokens_OpenIddictApplications_ApplicationId", + column: x => x.ApplicationId, + principalTable: "OpenIddictApplications", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_OpenIddictTokens_OpenIddictAuthorizations_AuthorizationId", + column: x => x.AuthorizationId, + principalTable: "OpenIddictAuthorizations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictApplications_ApplicationUserId", + table: "OpenIddictApplications", + column: "ApplicationUserId"); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictApplications_ClientId", + table: "OpenIddictApplications", + column: "ClientId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictAuthorizations_ApplicationId_Status_Subject_Type", + table: "OpenIddictAuthorizations", + columns: new[] { "ApplicationId", "Status", "Subject", "Type" }); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictScopes_Name", + table: "OpenIddictScopes", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_AuthorizationId", + table: "OpenIddictTokens", + column: "AuthorizationId"); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_ReferenceId", + table: "OpenIddictTokens", + column: "ReferenceId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_ApplicationId_Status_Subject_Type", + table: "OpenIddictTokens", + columns: new[] { "ApplicationId", "Status", "Subject", "Type" }); + } + } +} diff --git a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs index c71ba52bd..b2a26241a 100644 --- a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -160,164 +160,6 @@ namespace BTCPayServer.Migrations b.ToTable("AspNetUsers"); }); - modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdAuthorization", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("ApplicationId") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyToken") - .IsConcurrencyToken() - .HasColumnType("TEXT") - .HasMaxLength(50); - - b.Property("Properties") - .HasColumnType("TEXT"); - - b.Property("Scopes") - .HasColumnType("TEXT"); - - b.Property("Status") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(25); - - b.Property("Subject") - .HasColumnType("TEXT") - .HasMaxLength(450); - - b.Property("Type") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(25); - - b.HasKey("Id"); - - b.HasIndex("ApplicationId", "Status", "Subject", "Type"); - - b.ToTable("OpenIddictAuthorizations"); - }); - - modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdClient", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("ApplicationUserId") - .HasColumnType("TEXT"); - - b.Property("ClientId") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(100); - - b.Property("ClientSecret") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyToken") - .IsConcurrencyToken() - .HasColumnType("TEXT") - .HasMaxLength(50); - - b.Property("ConsentType") - .HasColumnType("TEXT"); - - b.Property("DisplayName") - .HasColumnType("TEXT"); - - b.Property("Permissions") - .HasColumnType("TEXT"); - - b.Property("PostLogoutRedirectUris") - .HasColumnType("TEXT"); - - b.Property("Properties") - .HasColumnType("TEXT"); - - b.Property("RedirectUris") - .HasColumnType("TEXT"); - - b.Property("Requirements") - .HasColumnType("TEXT"); - - b.Property("Type") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(25); - - b.HasKey("Id"); - - b.HasIndex("ApplicationUserId"); - - b.HasIndex("ClientId") - .IsUnique(); - - b.ToTable("OpenIddictApplications"); - }); - - modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdToken", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("ApplicationId") - .HasColumnType("TEXT"); - - b.Property("AuthorizationId") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyToken") - .IsConcurrencyToken() - .HasColumnType("TEXT") - .HasMaxLength(50); - - b.Property("CreationDate") - .HasColumnType("TEXT"); - - b.Property("ExpirationDate") - .HasColumnType("TEXT"); - - b.Property("Payload") - .HasColumnType("TEXT"); - - b.Property("Properties") - .HasColumnType("TEXT"); - - b.Property("ReferenceId") - .HasColumnType("TEXT") - .HasMaxLength(100); - - b.Property("Status") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(25); - - b.Property("Subject") - .HasColumnType("TEXT") - .HasMaxLength(450); - - b.Property("Type") - .IsRequired() - .HasColumnType("TEXT") - .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.HistoricalAddressInvoiceData", b => { b.Property("InvoiceDataId") @@ -812,42 +654,6 @@ namespace BTCPayServer.Migrations b.ToTable("AspNetUserTokens"); }); - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictScope", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("ConcurrencyToken") - .IsConcurrencyToken() - .HasColumnType("TEXT") - .HasMaxLength(50); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("DisplayName") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(200); - - b.Property("Properties") - .HasColumnType("TEXT"); - - b.Property("Resources") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("OpenIddictScopes"); - }); - modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => { b.HasOne("BTCPayServer.Data.StoreData", "StoreData") @@ -877,31 +683,6 @@ namespace BTCPayServer.Migrations .OnDelete(DeleteBehavior.Cascade); }); - modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdAuthorization", b => - { - b.HasOne("BTCPayServer.Data.BTCPayOpenIdClient", "Application") - .WithMany("Authorizations") - .HasForeignKey("ApplicationId"); - }); - - modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdClient", b => - { - b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") - .WithMany("OpenIdClients") - .HasForeignKey("ApplicationUserId"); - }); - - modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdToken", b => - { - b.HasOne("BTCPayServer.Data.BTCPayOpenIdClient", "Application") - .WithMany("Tokens") - .HasForeignKey("ApplicationId"); - - b.HasOne("BTCPayServer.Data.BTCPayOpenIdAuthorization", "Authorization") - .WithMany("Tokens") - .HasForeignKey("AuthorizationId"); - }); - modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => { b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") diff --git a/BTCPayServer.Tests/AuthenticationTests.cs b/BTCPayServer.Tests/AuthenticationTests.cs deleted file mode 100644 index 805db57f8..000000000 --- a/BTCPayServer.Tests/AuthenticationTests.cs +++ /dev/null @@ -1,428 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using System.Security.Claims; -using BTCPayServer.Tests.Logging; -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; -using Microsoft.AspNetCore.Identity; - -namespace BTCPayServer.Tests -{ - public class AuthenticationTests - { - public const string TestApiPath = "api/test/openid"; - public const int TestTimeout = TestUtils.TestTimeout; - public AuthenticationTests(ITestOutputHelper helper) - { - Logs.Tester = new XUnitLog(helper) {Name = "Tests"}; - Logs.LogProvider = new XUnitLogProvider(helper); - } - - [Fact(Timeout = TestTimeout)] - [Trait("Integration", "Integration")] - public async Task GetRedirectedToLoginPathOnChallenge() - { - using (var tester = ServerTester.Create()) - { - await tester.StartAsync(); - 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(Timeout = TestTimeout)] - [Trait("Integration", "Integration")] - public async Task CanGetOpenIdConfiguration() - { - using (var tester = ServerTester.Create()) - { - await tester.StartAsync(); - 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); - JObject.Parse(json); // Should do more tests but good enough - } - } - } - } - - [Fact(Timeout = TestTimeout)] - [Trait("Integration", "Integration")] - public async Task CanUseNonInteractiveFlows() - { - using (var tester = ServerTester.Create()) - { - await tester.StartAsync(); - - var user = tester.NewAccount(); - user.GrantAccess(); - await user.MakeAdmin(); - 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); - } - } - - [Fact(Timeout = TestTimeout)] - [Trait("Selenium", "Selenium")] - public async Task CanUseImplicitFlow() - { - using (var s = SeleniumTester.Create()) - { - await s.StartAsync(); - var tester = s.Server; - - var user = tester.NewAccount(); - user.GrantAccess(); - await user.MakeAdmin(); - 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 server_management store_management&nonce={Guid.NewGuid().ToString()}"); - s.Driver.Navigate().GoToUrl(implicitAuthorizeUrl); - s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password); - s.Driver.FindElement(By.Id("consent-yes")).Click(); - 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(implicitAuthorizeUrlSilentModel); - 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); - - var stores = await TestApiAgainstAccessToken(results["access_token"], - $"{TestApiPath}/me/stores", - tester.PayTester.HttpClient); - Assert.NotEmpty(stores); - - Assert.True(await TestApiAgainstAccessToken(results["access_token"], - $"{TestApiPath}/me/stores/{stores[0].Id}/can-edit", - tester.PayTester.HttpClient)); - - //we dont ask for consent after acquiring it the first time for the same scopes. - LogoutFlow(tester, id, s); - s.Driver.Navigate().GoToUrl(implicitAuthorizeUrl); - s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password); - s.Driver.AssertElementNotFound(By.Id("consent-yes")); - - // Let's asks without scopes - LogoutFlow(tester, id, s); - id = Guid.NewGuid().ToString(); - openIdClient = await user.RegisterOpenIdClient( - new OpenIddictApplicationDescriptor() - { - ClientId = id, - DisplayName = id, - Permissions = { OpenIddictConstants.Permissions.GrantTypes.Implicit }, - RedirectUris = { redirecturi }, - }); - 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); - s.Driver.FindElement(By.Id("consent-yes")).Click(); - results = s.Driver.Url.Split("#").Last().Split("&") - .ToDictionary(s1 => s1.Split("=")[0], s1 => s1.Split("=")[1]); - - await Assert.ThrowsAnyAsync(async () => - { - await TestApiAgainstAccessToken(results["access_token"], - $"{TestApiPath}/me/stores", - tester.PayTester.HttpClient); - }); - await Assert.ThrowsAnyAsync(async () => - { - await TestApiAgainstAccessToken(results["access_token"], - $"{TestApiPath}/me/stores/{stores[0].Id}/can-edit", - tester.PayTester.HttpClient); - }); - } - } - - 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(() => seleniumTester.Driver.FindElement(By.Id("Logout"))); - - } - - [Fact(Timeout = TestTimeout)] - [Trait("Selenium", "Selenium")] - public async Task CanUseCodeFlow() - { - using (var s = SeleniumTester.Create()) - { - await s.StartAsync(); - var tester = s.Server; - - var user = tester.NewAccount(); - user.GrantAccess(); - await user.MakeAdmin(); - 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 server_management store_management&state={Guid.NewGuid().ToString()}"); - s.Driver.Navigate().GoToUrl(authorizeUrl); - s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password); - s.Driver.FindElement(By.Id("consent-yes")).Click(); - 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>() - { - new KeyValuePair("grant_type", - OpenIddictConstants.GrantTypes.AuthorizationCode), - new KeyValuePair("client_id", openIdClient.ClientId), - new KeyValuePair("client_secret", secret), - new KeyValuePair("code", results["code"]), - new KeyValuePair("redirect_uri", redirecturi.AbsoluteUri) - }) - }; - - - var response = await httpClient.SendAsync(httpRequest); - - Assert.True(response.IsSuccessStatusCode); - - string content = await response.Content.ReadAsStringAsync(); - var result = System.Text.Json.JsonSerializer.Deserialize(content); - - await TestApiAgainstAccessToken(result.AccessToken, tester, user); - - var refreshedAccessToken = await RefreshAnAccessToken(result.RefreshToken, httpClient, id, secret); - - await TestApiAgainstAccessToken(refreshedAccessToken, tester, user); - - LogoutFlow(tester, id, s); - s.Driver.Navigate().GoToUrl(authorizeUrl); - s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password); - - Assert.Throws(() => s.Driver.FindElement(By.Id("consent-yes"))); - results = url.Split("?").Last().Split("&") - .ToDictionary(s1 => s1.Split("=")[0], s1 => s1.Split("=")[1]); - Assert.True(results.ContainsKey("code")); - } - } - - private static async Task 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>() - { - new KeyValuePair("grant_type", - OpenIddictConstants.GrantTypes.RefreshToken), - new KeyValuePair("client_id", clientId), - new KeyValuePair("client_secret", clientSecret), - new KeyValuePair("refresh_token", refreshToken) - }) - }; - - var response = await client.SendAsync(httpRequest); - - Assert.True(response.IsSuccessStatusCode); - - string content = await response.Content.ReadAsStringAsync(); - var result = System.Text.Json.JsonSerializer.Deserialize(content); - Assert.NotEmpty(result.AccessToken); - Assert.Null(result.Error); - return result.AccessToken; - } - - private static async Task 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>() - { - new KeyValuePair("grant_type", - OpenIddictConstants.GrantTypes.ClientCredentials), - new KeyValuePair("client_id", openIdClient.ClientId), - new KeyValuePair("client_secret", secret), - new KeyValuePair("scope", "server_management store_management") - }) - }; - - - var response = await httpClient.SendAsync(httpRequest); - - Assert.True(response.IsSuccessStatusCode); - - string content = await response.Content.ReadAsStringAsync(); - var result = System.Text.Json.JsonSerializer.Deserialize(content); - Assert.NotEmpty(result.AccessToken); - Assert.Null(result.Error); - return result.AccessToken; - } - - private static async Task 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>() - { - new KeyValuePair("grant_type", OpenIddictConstants.GrantTypes.Password), - new KeyValuePair("username", user.RegisterDetails.Email), - new KeyValuePair("password", user.RegisterDetails.Password), - new KeyValuePair("client_id", openIdClient.ClientId), - new KeyValuePair("client_secret", secret), - new KeyValuePair("scope", "server_management store_management") - }) - }; - - - var response = await httpClient.SendAsync(httpRequest); - - Assert.True(response.IsSuccessStatusCode); - - string content = await response.Content.ReadAsStringAsync(); - var result = System.Text.Json.JsonSerializer.Deserialize(content); - Assert.NotEmpty(result.AccessToken); - Assert.Null(result.Error); - return result.AccessToken; - } - - async Task TestApiAgainstAccessToken(string accessToken, ServerTester tester, TestAccount testAccount) - { - var resultUser = - await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/id", - tester.PayTester.HttpClient); - Assert.Equal(testAccount.UserId, resultUser); - - var secondUser = tester.NewAccount(); - secondUser.GrantAccess(); - - var resultStores = - await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/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(accessToken, - $"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit", - tester.PayTester.HttpClient)); - - Assert.True(await TestApiAgainstAccessToken(accessToken, - $"{TestApiPath}/me/is-admin", - tester.PayTester.HttpClient)); - - await Assert.ThrowsAnyAsync(async () => - { - await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/stores/{secondUser.StoreId}/can-edit", - tester.PayTester.HttpClient); - }); - } - - public async Task TestApiAgainstAccessToken(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(rawJson); - } - } -} diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index 39c51b2f2..c0d6fed70 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -32,7 +32,6 @@ using System.Security.Claims; using System.Security.Principal; using System.Text; using System.Threading; -using OpenIddict.Abstractions; using Xunit; using BTCPayServer.Services; using System.Net.Http; @@ -298,7 +297,7 @@ namespace BTCPayServer.Tests if (userId != null) { List claims = new List(); - claims.Add(new Claim(OpenIddictConstants.Claims.Subject, userId)); + claims.Add(new Claim(ClaimTypes.NameIdentifier, userId)); if (isAdmin) claims.Add(new Claim(ClaimTypes.Role, Roles.ServerAdmin)); context.User = new ClaimsPrincipal(new ClaimsIdentity(claims.ToArray(), AuthenticationSchemes.Cookie)); diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index f73e155cc..d57eb54df 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -18,8 +18,6 @@ using BTCPayServer.Tests.Logging; using BTCPayServer.Lightning; using BTCPayServer.Lightning.CLightning; using BTCPayServer.Data; -using OpenIddict.Abstractions; -using OpenIddict.Core; using Microsoft.AspNetCore.Identity; using NBXplorer.Models; @@ -194,14 +192,5 @@ namespace BTCPayServer.Tests if (storeController.ModelState.ErrorCount != 0) Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage); } - - public async Task RegisterOpenIdClient(OpenIddictApplicationDescriptor descriptor, string secret = null) - { - var openIddictApplicationManager = parent.PayTester.GetService>(); - var client = new BTCPayOpenIdClient { Id = Guid.NewGuid().ToString(), ApplicationUserId = UserId}; - await openIddictApplicationManager.PopulateAsync(client, descriptor); - await openIddictApplicationManager.CreateAsync(client, secret); - return client; - } } } diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index e2e795e75..749d9db8f 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -66,9 +66,6 @@ - - - @@ -220,4 +217,8 @@ $(IncludeRazorContentInPack) + + + <_ContentIncludedByDefault Remove="Views\Authorization\Authorize.cshtml" /> + diff --git a/BTCPayServer/Controllers/AuthorizationController.cs b/BTCPayServer/Controllers/AuthorizationController.cs deleted file mode 100644 index 21396e2c8..000000000 --- a/BTCPayServer/Controllers/AuthorizationController.cs +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Collections.Immutable; -using System.Linq; -using System.Threading.Tasks; -using BTCPayServer.Security.OpenId; -using BTCPayServer.Data; -using BTCPayServer.Models; -using BTCPayServer.Models.Authorization; -using BTCPayServer.Security; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using Microsoft.AspNetCore; -using OpenIddict.Abstractions; -using OpenIddict.Core; -using OpenIddict.Server; -using System.Security.Claims; -using OpenIddict.Server.AspNetCore; - -namespace BTCPayServer.Controllers -{ - public class AuthorizationController : Controller - { - private readonly OpenIddictApplicationManager _applicationManager; - private readonly SignInManager _signInManager; - private readonly OpenIddictAuthorizationManager _authorizationManager; - private readonly UserManager _userManager; - private readonly IOptions _IdentityOptions; - - public AuthorizationController( - OpenIddictApplicationManager applicationManager, - SignInManager signInManager, - OpenIddictAuthorizationManager authorizationManager, - UserManager userManager, - IOptions identityOptions) - { - _applicationManager = applicationManager; - _signInManager = signInManager; - _authorizationManager = authorizationManager; - _userManager = userManager; - _IdentityOptions = identityOptions; - } - - [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] - [HttpGet("/connect/authorize")] - public async Task Authorize() - { - var request = HttpContext.GetOpenIddictServerRequest(); - // Retrieve the application details from the database. - var application = await _applicationManager.FindByClientIdAsync(request.ClientId); - - if (application == null) - { - return View("Error", - new ErrorViewModel - { - Error = OpenIddictConstants.Errors.InvalidClient, - ErrorDescription = - "Details concerning the calling client application cannot be found in the database" - }); - } - - var userId = _userManager.GetUserId(User); - if (!string.IsNullOrEmpty( - await OpenIdExtensions.IsUserAuthorized(_authorizationManager, request, userId, application.Id))) - { - return await Authorize("YES", false); - } - - // Flow the request_id to allow OpenIddict to restore - // the original authorization request from the cache. - return View(new AuthorizeViewModel - { - ApplicationName = await _applicationManager.GetDisplayNameAsync(application), - RequestId = request.RequestId, - Scope = request.GetScopes() - }); - } - - [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] - [HttpPost("/connect/authorize")] - public async Task Authorize(string consent, bool createAuthorization = true) - { - var request = HttpContext.GetOpenIddictServerRequest(); - var user = await _userManager.GetUserAsync(User); - if (user == null) - { - return View("Error", - new ErrorViewModel - { - Error = OpenIddictConstants.Errors.ServerError, - ErrorDescription = "The specified user could not be found" - }); - } - - string type = null; - switch (consent.ToUpperInvariant()) - { - case "YESTEMPORARY": - type = OpenIddictConstants.AuthorizationTypes.AdHoc; - break; - case "YES": - type = OpenIddictConstants.AuthorizationTypes.Permanent; - break; - case "NO": - default: - // Notify OpenIddict that the authorization grant has been denied by the resource owner - // to redirect the user agent to the client application using the appropriate response_mode. - return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - } - - - var principal = await _signInManager.CreateUserPrincipalAsync(user); - principal = await _signInManager.CreateUserPrincipalAsync(user); - principal.SetScopes(request.GetScopes().Restrict(principal)); - principal.SetDestinations(_IdentityOptions.Value); - if (createAuthorization) - { - var application = await _applicationManager.FindByClientIdAsync(request.ClientId); - var authorization = await _authorizationManager.CreateAsync(User, user.Id, application.Id, - type, principal.GetScopes()); - principal.SetInternalAuthorizationId(authorization.Id); - } - - // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. - return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - } - } -} diff --git a/BTCPayServer/Controllers/RestApi/TestOpenIdController.cs b/BTCPayServer/Controllers/RestApi/TestOpenIdController.cs deleted file mode 100644 index 0e5623d29..000000000 --- a/BTCPayServer/Controllers/RestApi/TestOpenIdController.cs +++ /dev/null @@ -1,65 +0,0 @@ -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.AspNetCore; - -namespace BTCPayServer.Controllers.RestApi -{ - /// - /// this controller serves as a testing endpoint for our OpenId unit tests - /// - [Route("api/test/openid")] - [ApiController] - [Authorize(AuthenticationSchemes = AuthenticationSchemes.OpenId)] - public class TestOpenIdController : ControllerBase - { - private readonly UserManager _userManager; - private readonly StoreRepository _storeRepository; - - public TestOpenIdController(UserManager userManager, StoreRepository storeRepository) - { - _userManager = userManager; - _storeRepository = storeRepository; - } - - [HttpGet("me/id")] - public string GetCurrentUserId() - { - return _userManager.GetUserId(User); - } - - [HttpGet("me")] - public async Task GetCurrentUser() - { - return await _userManager.GetUserAsync(User); - } - - [HttpGet("me/is-admin")] - [Authorize(Policy = Policies.CanModifyServerSettings.Key, AuthenticationSchemes = AuthenticationSchemes.OpenId)] - public bool AmIAnAdmin() - { - return true; - } - - [HttpGet("me/stores")] - [Authorize(Policy = Policies.CanModifyStoreSettings.Key, - AuthenticationSchemes = AuthenticationSchemes.OpenId)] - public async Task GetCurrentUserStores() - { - return await _storeRepository.GetStoresByUserId(_userManager.GetUserId(User)); - } - - [HttpGet("me/stores/{storeId}/can-edit")] - [Authorize(Policy = Policies.CanModifyStoreSettings.Key, - AuthenticationSchemes = AuthenticationSchemes.OpenId)] - public bool CanEdit(string storeId) - { - return true; - } - } -} diff --git a/BTCPayServer/Extensions/OpenIddictExtensions.cs b/BTCPayServer/Extensions/OpenIddictExtensions.cs deleted file mode 100644 index 93814bb34..000000000 --- a/BTCPayServer/Extensions/OpenIddictExtensions.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.IO; -using System.Security.Cryptography; -using BTCPayServer.Configuration; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.IdentityModel.Tokens; -using NETCore.Encrypt.Extensions.Internal; - -namespace BTCPayServer -{ - public static class OpenIddictExtensions - { - public static SecurityKey GetSigningKey(IConfiguration configuration, string fileName) - { - - var file = Path.Combine(configuration.GetDataDir(), fileName); - using var rsa = new RSACryptoServiceProvider(2048); - if (File.Exists(file)) - { - rsa.FromXmlString2(File.ReadAllText(file)); - } - else - { - var contents = rsa.ToXmlString2(true); - File.WriteAllText(file, contents); - } - return new RsaSecurityKey(rsa.ExportParameters(true));; - } - public static OpenIddictServerBuilder ConfigureSigningKey(this OpenIddictServerBuilder builder, - IConfiguration configuration) - { - return builder - .AddSigningKey(GetSigningKey(configuration, "signing.rsaparams")) - .AddEncryptionKey(GetSigningKey(configuration, "encrypting.rsaparams")); - } - } -} diff --git a/BTCPayServer/Extensions/RsaKeyExtensions.cs b/BTCPayServer/Extensions/RsaKeyExtensions.cs deleted file mode 100644 index c201917bb..000000000 --- a/BTCPayServer/Extensions/RsaKeyExtensions.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Globalization; -using System.Security.Cryptography; -using System.Xml; - -namespace NETCore.Encrypt.Extensions.Internal -{ - /// - /// .net core's implementatiosn are still marked as unsupported because of stupid decisions( https://github.com/dotnet/corefx/issues/23686) - /// - internal static class RsaKeyExtensions - { - #region XML - - public static void FromXmlString2(this RSA rsa, string xmlString) - { - RSAParameters parameters = new RSAParameters(); - - XmlDocument xmlDoc = new XmlDocument(); - xmlDoc.LoadXml(xmlString); - - if (xmlDoc.DocumentElement.Name.Equals("RSAKeyValue", StringComparison.InvariantCulture)) - { - foreach (XmlNode node in xmlDoc.DocumentElement.ChildNodes) - { - switch (node.Name) - { - case "Modulus": - parameters.Modulus = (string.IsNullOrEmpty(node.InnerText) - ? null - : Convert.FromBase64String(node.InnerText)); - break; - case "Exponent": - parameters.Exponent = (string.IsNullOrEmpty(node.InnerText) - ? null - : Convert.FromBase64String(node.InnerText)); - break; - case "P": - parameters.P = (string.IsNullOrEmpty(node.InnerText) - ? null - : Convert.FromBase64String(node.InnerText)); - break; - case "Q": - parameters.Q = (string.IsNullOrEmpty(node.InnerText) - ? null - : Convert.FromBase64String(node.InnerText)); - break; - case "DP": - parameters.DP = (string.IsNullOrEmpty(node.InnerText) - ? null - : Convert.FromBase64String(node.InnerText)); - break; - case "DQ": - parameters.DQ = (string.IsNullOrEmpty(node.InnerText) - ? null - : Convert.FromBase64String(node.InnerText)); - break; - case "InverseQ": - parameters.InverseQ = (string.IsNullOrEmpty(node.InnerText) - ? null - : Convert.FromBase64String(node.InnerText)); - break; - case "D": - parameters.D = (string.IsNullOrEmpty(node.InnerText) - ? null - : Convert.FromBase64String(node.InnerText)); - break; - } - } - } - else - { - throw new Exception("Invalid XML RSA key."); - } - - rsa.ImportParameters(parameters); - } - - public static string ToXmlString2(this RSA rsa, bool includePrivateParameters) - { - RSAParameters parameters = rsa.ExportParameters(includePrivateParameters); - - return string.Format(CultureInfo.InvariantCulture, - "{0}{1}

    {2}

    {3}{4}{5}{6}{7}
    ", - parameters.Modulus != null ? Convert.ToBase64String(parameters.Modulus) : null, - parameters.Exponent != null ? Convert.ToBase64String(parameters.Exponent) : null, - parameters.P != null ? Convert.ToBase64String(parameters.P) : null, - parameters.Q != null ? Convert.ToBase64String(parameters.Q) : null, - parameters.DP != null ? Convert.ToBase64String(parameters.DP) : null, - parameters.DQ != null ? Convert.ToBase64String(parameters.DQ) : null, - parameters.InverseQ != null ? Convert.ToBase64String(parameters.InverseQ) : null, - parameters.D != null ? Convert.ToBase64String(parameters.D) : null); - } - - #endregion - } -} diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index a9064f044..7072b63e9 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -41,17 +41,6 @@ using Npgsql; using BTCPayServer.Services.Apps; using BTCPayServer.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.Hosting; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using BTCPayServer.Security.Bitpay; using Serilog; @@ -67,7 +56,6 @@ namespace BTCPayServer.Hosting { var factory = provider.GetRequiredService(); factory.ConfigureBuilder(o); - o.UseOpenIddict, BTCPayOpenIdToken, string>(); }); services.AddHttpClient(); services.AddHttpClient(nameof(ExplorerClientProvider), httpClient => @@ -221,7 +209,6 @@ namespace BTCPayServer.Hosting services.AddSingleton(); services.AddSingleton(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.TryAddSingleton(); diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index a88e7f037..86eadaa71 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -1,8 +1,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Hosting; -using OpenIddict.Validation.AspNetCore; -using OpenIddict.Abstractions; + using Microsoft.AspNetCore.Builder; using System; using Microsoft.Extensions.DependencyInjection; @@ -19,14 +18,10 @@ using System.IO; using Microsoft.Extensions.DependencyInjection.Extensions; using BTCPayServer.Security; using Microsoft.AspNetCore.Server.Kestrel.Core; -using OpenIddict.EntityFrameworkCore.Models; using System.Net; -using BTCPayServer.Security.OpenId; using BTCPayServer.PaymentRequest; using BTCPayServer.Services.Apps; using BTCPayServer.Storage; -using Microsoft.Extensions.Options; -using OpenIddict.Core; namespace BTCPayServer.Hosting { @@ -57,8 +52,6 @@ namespace BTCPayServer.Hosting .AddEntityFrameworkStores() .AddDefaultTokenProviders(); - ConfigureOpenIddict(services); - services.AddBTCPayServer(Configuration); services.AddProviderStorage(); services.AddSession(); @@ -95,12 +88,6 @@ namespace BTCPayServer.Hosting options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.AllowedForNewUsers = true; options.Password.RequireUppercase = false; - // Configure Identity to use the same JWT claims as OpenIddict instead - // of the legacy WS-Federation claims it uses by default (ClaimTypes), - // which saves you from doing the mapping in your authorization controller. - options.ClaimsIdentity.UserNameClaimType = OpenIddictConstants.Claims.Name; - options.ClaimsIdentity.UserIdClaimType = OpenIddictConstants.Claims.Subject; - options.ClaimsIdentity.RoleClaimType = OpenIddictConstants.Claims.Role; }); // If the HTTPS certificate path is not set this logic will NOT be used and the default Kestrel binding logic will be. string httpsCertificateFilePath = Configuration.GetOrDefault("HttpsCertificateFilePath", null); @@ -143,67 +130,6 @@ namespace BTCPayServer.Hosting }); } } - - private DirectoryInfo GetDataDir() - { - return new DirectoryInfo(Configuration.GetDataDir(DefaultConfiguration.GetNetworkType(Configuration))); - } - - private void ConfigureOpenIddict(IServiceCollection services) - { - // Register the OpenIddict services. - services.AddOpenIddict() - .AddCore(options => - { - // Configure OpenIddict to use the Entity Framework Core stores and entities. - options.UseEntityFrameworkCore() - .UseDbContext() - .ReplaceDefaultEntities, - BTCPayOpenIdToken, string>(); - }) - .AddServer(options => - { - options.UseAspNetCore() - .EnableStatusCodePagesIntegration() - .EnableAuthorizationEndpointPassthrough() - .EnableLogoutEndpointPassthrough() - .EnableAuthorizationEndpointCaching() - .DisableTransportSecurityRequirement(); - - // Enable the token endpoint (required to use the password flow). - options.SetTokenEndpointUris("/connect/token"); - options.SetAuthorizationEndpointUris("/connect/authorize"); - options.SetLogoutEndpointUris("/connect/logout"); - - //we do not care about these granular controls for now - options.IgnoreScopePermissions(); - options.IgnoreEndpointPermissions(); - // Allow client applications various flows - options.AllowImplicitFlow(); - options.AllowClientCredentialsFlow(); - options.AllowRefreshTokenFlow(); - options.AllowPasswordFlow(); - options.AllowAuthorizationCodeFlow(); - options.UseRollingTokens(); - - options.RegisterScopes( - OpenIddictConstants.Scopes.OpenId, - BTCPayScopes.StoreManagement, - BTCPayScopes.ServerManagement - ); - options.AddEventHandler(PasswordGrantTypeEventHandler.Descriptor); - options.AddEventHandler(OpenIdGrantHandlerCheckCanSignIn.Descriptor); - options.AddEventHandler(ClientCredentialsGrantTypeEventHandler.Descriptor); - options.AddEventHandler(LogoutEventHandler.Descriptor); - options.ConfigureSigningKey(Configuration); - }) - .AddValidation(options => - { - options.UseLocalServer(); - options.UseAspNetCore(); - }); - } - public void Configure( IApplicationBuilder app, IWebHostEnvironment env, @@ -224,6 +150,10 @@ namespace BTCPayServer.Hosting }); } } + private DirectoryInfo GetDataDir() + { + return new DirectoryInfo(Configuration.GetDataDir(DefaultConfiguration.GetNetworkType(Configuration))); + } private static void ConfigureCore(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider prov, ILoggerFactory loggerFactory, BTCPayServerOptions options) { diff --git a/BTCPayServer/Program.cs b/BTCPayServer/Program.cs index 69bb07d2e..0f3d9cb4d 100644 --- a/BTCPayServer/Program.cs +++ b/BTCPayServer/Program.cs @@ -53,7 +53,6 @@ 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("OpenIddict.Server.OpenIddictServerProvider", LogLevel.Error); l.AddProvider(new CustomConsoleLogProvider(processor)); }) .UseStartup() diff --git a/BTCPayServer/Security/AuthenticationSchemes.cs b/BTCPayServer/Security/AuthenticationSchemes.cs index a3117ca3a..93bcfedc5 100644 --- a/BTCPayServer/Security/AuthenticationSchemes.cs +++ b/BTCPayServer/Security/AuthenticationSchemes.cs @@ -1,17 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using OpenIddict.Validation; -using OpenIddict.Validation.AspNetCore; - -namespace BTCPayServer.Security +namespace BTCPayServer.Security { public class AuthenticationSchemes { public const string Cookie = "Identity.Application"; public const string Bitpay = "Bitpay"; - public const string OpenId = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme; public const string ApiKey = "GreenfieldApiKey"; } } diff --git a/BTCPayServer/Security/OpenId/BTCPayScopes.cs b/BTCPayServer/Security/OpenId/BTCPayScopes.cs deleted file mode 100644 index 71d92e24e..000000000 --- a/BTCPayServer/Security/OpenId/BTCPayScopes.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using OpenIddict.Abstractions; - -namespace BTCPayServer.Security.OpenId -{ - public static class BTCPayScopes - { - public const string StoreManagement = "store_management"; - public const string ServerManagement = "server_management"; - } - public static class RestAPIPolicies - { - - } -} diff --git a/BTCPayServer/Security/OpenId/BaseOpenIdGrantHandler.cs b/BTCPayServer/Security/OpenId/BaseOpenIdGrantHandler.cs deleted file mode 100644 index 1dfc4a071..000000000 --- a/BTCPayServer/Security/OpenId/BaseOpenIdGrantHandler.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Threading.Tasks; -using OpenIdConnectRequest = OpenIddict.Abstractions.OpenIddictRequest; -using BTCPayServer.Data; -using BTCPayServer.Models; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Options; -using OpenIddict.Core; -using OpenIddict.Server; -using System.Security.Claims; - -namespace BTCPayServer.Security.OpenId -{ - public abstract class BaseOpenIdGrantHandler : - IOpenIddictServerHandler - where T : OpenIddictServerEvents.BaseContext - { - private readonly OpenIddictApplicationManager _applicationManager; - private readonly OpenIddictAuthorizationManager _authorizationManager; - protected readonly SignInManager _signInManager; - protected readonly IOptions _identityOptions; - - protected BaseOpenIdGrantHandler( - OpenIddictApplicationManager applicationManager, - OpenIddictAuthorizationManager authorizationManager, - SignInManager signInManager, - IOptions identityOptions) - { - _applicationManager = applicationManager; - _authorizationManager = authorizationManager; - _signInManager = signInManager; - _identityOptions = identityOptions; - } - - - protected Task CreateClaimsPrincipalAsync(OpenIdConnectRequest request, ApplicationUser user) - { - return OpenIdExtensions.CreateClaimsPrincipalAsync(_applicationManager, _authorizationManager, - _identityOptions.Value, _signInManager, request, user); - } - public abstract ValueTask HandleAsync(T notification); - } -} diff --git a/BTCPayServer/Security/OpenId/ClientCredentialsGrantTypeEventHandler.cs b/BTCPayServer/Security/OpenId/ClientCredentialsGrantTypeEventHandler.cs deleted file mode 100644 index af598a44a..000000000 --- a/BTCPayServer/Security/OpenId/ClientCredentialsGrantTypeEventHandler.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Security.Claims; -using System.Threading.Tasks; -using BTCPayServer.Data; -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.Security.OpenId -{ - public class ClientCredentialsGrantTypeEventHandler : - BaseOpenIdGrantHandler - { - public static OpenIddictServerHandlerDescriptor Descriptor { get; } = - OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseScopedHandler() - .Build(); - private readonly OpenIddictApplicationManager _applicationManager; - - private readonly UserManager _userManager; - - public ClientCredentialsGrantTypeEventHandler( - OpenIddictApplicationManager applicationManager, - OpenIddictAuthorizationManager authorizationManager, - SignInManager signInManager, - IOptions identityOptions, - UserManager userManager) : base(applicationManager, authorizationManager, signInManager, - identityOptions) - { - _applicationManager = applicationManager; - _userManager = userManager; - } - - public override async ValueTask HandleAsync( - OpenIddictServerEvents.HandleTokenRequestContext notification) - { - var request = notification.Request; - var context = notification; - if (!request.IsClientCredentialsGrantType()) - { - return; - } - - var application = await _applicationManager.FindByClientIdAsync(request.ClientId); - if (application == null) - { - context.Reject( - error: OpenIddictConstants.Errors.InvalidClient, - description: "The client application was not found in the database."); - return; - } - - var user = await _userManager.FindByIdAsync(application.ApplicationUserId); - context.Principal = await CreateClaimsPrincipalAsync(request, user); - } - } -} diff --git a/BTCPayServer/Security/OpenId/LogoutEventHandler.cs b/BTCPayServer/Security/OpenId/LogoutEventHandler.cs deleted file mode 100644 index fc42d0e6c..000000000 --- a/BTCPayServer/Security/OpenId/LogoutEventHandler.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Threading.Tasks; -using BTCPayServer.Data; -using BTCPayServer.Models; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Options; -using OpenIddict.Core; -using OpenIddict.Server; -using Microsoft.AspNetCore; -using OpenIddict.Server.AspNetCore; - -namespace BTCPayServer.Security.OpenId -{ - public class LogoutEventHandler : IOpenIddictServerHandler - { - protected readonly SignInManager _signInManager; - public static OpenIddictServerHandlerDescriptor Descriptor { get; } = - OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseScopedHandler() - .Build(); - public LogoutEventHandler(SignInManager signInManager) - { - _signInManager = signInManager; - } - - public async ValueTask HandleAsync( - OpenIddictServerEvents.HandleLogoutRequestContext notification) - { - await _signInManager.SignOutAsync(); - } - } -} diff --git a/BTCPayServer/Security/OpenId/OpenIdExtensions.cs b/BTCPayServer/Security/OpenId/OpenIdExtensions.cs deleted file mode 100644 index 12ef9e83b..000000000 --- a/BTCPayServer/Security/OpenId/OpenIdExtensions.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using BTCPayServer.Data; -using BTCPayServer.Models; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Identity; -using OpenIddict.Abstractions; -using OpenIddict.Core; -using OpenIddict.Server; - -namespace BTCPayServer.Security.OpenId -{ - public static class OpenIdExtensions - { - public static ImmutableHashSet Restrict(this ImmutableArray scopes, ClaimsPrincipal claimsPrincipal) - { - HashSet restricted = new HashSet(); - foreach (var scope in scopes) - { - if (scope == BTCPayScopes.ServerManagement && !claimsPrincipal.IsInRole(Roles.ServerAdmin)) - continue; - restricted.Add(scope); - } - return restricted.ToImmutableHashSet(); - } - public static async Task CreateClaimsPrincipalAsync(OpenIddictApplicationManager applicationManager, - OpenIddictAuthorizationManager authorizationManager, - IdentityOptions identityOptions, - SignInManager signInManager, - OpenIddictRequest request, - ApplicationUser user) - { - var principal = await signInManager.CreateUserPrincipalAsync(user); - if (!request.IsAuthorizationCodeGrantType() && !request.IsRefreshTokenGrantType()) - { - principal.SetScopes(request.GetScopes().Restrict(principal)); - } - else if (request.IsAuthorizationCodeGrantType() && - string.IsNullOrEmpty(principal.GetInternalAuthorizationId())) - { - var app = await applicationManager.FindByClientIdAsync(request.ClientId); - var authorizationId = await IsUserAuthorized(authorizationManager, request, user.Id, app.Id); - if (!string.IsNullOrEmpty(authorizationId)) - { - principal.SetInternalAuthorizationId(authorizationId); - } - } - - principal.SetDestinations(identityOptions); - return principal; - } - - public static void SetDestinations(this ClaimsPrincipal principal, IdentityOptions identityOptions) - { - foreach (var claim in principal.Claims) - { - claim.SetDestinations(GetDestinations(identityOptions, claim, principal)); - } - } - - private static IEnumerable GetDestinations(IdentityOptions identityOptions, Claim claim, - ClaimsPrincipal principal) - { - switch (claim.Type) - { - case OpenIddictConstants.Claims.Name: - case OpenIddictConstants.Claims.Email: - yield return OpenIddictConstants.Destinations.AccessToken; - yield break; - } - } - - public static async Task IsUserAuthorized( - OpenIddictAuthorizationManager authorizationManager, - OpenIddictRequest request, string userId, string applicationId) - { - var authorizations = await authorizationManager.ListAsync(queryable => - queryable.Where(authorization => - authorization.Subject == userId && - authorization.Application.Id == applicationId && - authorization.Status == OpenIddictConstants.Statuses.Valid)).ToArrayAsync(); - - if (authorizations.Length > 0) - { - var scopeTasks = authorizations.Select(authorization => - (authorizationManager.GetScopesAsync(authorization).AsTask(), authorization.Id)); - await Task.WhenAll(scopeTasks.Select((tuple) => tuple.Item1)); - - var authorizationsWithSufficientScopes = scopeTasks - .Select((tuple) => (Id: tuple.Id, Scopes: tuple.Item1.Result)) - .Where((tuple) => !request.GetScopes().Except(tuple.Scopes).Any()); - - if (authorizationsWithSufficientScopes.Any()) - { - return authorizationsWithSufficientScopes.First().Id; - } - } - - return null; - } - } -} diff --git a/BTCPayServer/Security/OpenId/OpenIdGrantHandlerCheckCanSignIn.cs b/BTCPayServer/Security/OpenId/OpenIdGrantHandlerCheckCanSignIn.cs deleted file mode 100644 index cd353d65d..000000000 --- a/BTCPayServer/Security/OpenId/OpenIdGrantHandlerCheckCanSignIn.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Threading.Tasks; -using OpenIddict.Abstractions; -using BTCPayServer.Data; -using BTCPayServer.Models; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Options; -using OpenIddict.Core; -using OpenIddict.Server; -using Microsoft.AspNetCore; -using OpenIddict.Server.AspNetCore; - -namespace BTCPayServer.Security.OpenId -{ - public class OpenIdGrantHandlerCheckCanSignIn : - BaseOpenIdGrantHandler - { - public static OpenIddictServerHandlerDescriptor Descriptor { get; } = - OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseScopedHandler() - .Build(); - - private readonly UserManager _userManager; - - public OpenIdGrantHandlerCheckCanSignIn( - OpenIddictApplicationManager applicationManager, - OpenIddictAuthorizationManager authorizationManager, - SignInManager signInManager, - IOptions identityOptions, UserManager userManager) : base( - applicationManager, authorizationManager, signInManager, - identityOptions) - { - _userManager = userManager; - } - - public override async ValueTask HandleAsync( - OpenIddictServerEvents.HandleTokenRequestContext notification) - { - var request = notification.Request; - if (!request.IsRefreshTokenGrantType() && !request.IsAuthorizationCodeGrantType()) - { - // Allow other handlers to process the event. - return; - } - - var httpContext = notification.Transaction.GetHttpRequest().HttpContext; - var authenticateResult = (await httpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)); - - var user = await _userManager.GetUserAsync(authenticateResult.Principal); - if (user == null) - { - notification.Reject( - error: OpenIddictConstants.Errors.InvalidGrant, - description: "The token is no longer valid."); - return; - } - - // Ensure the user is still allowed to sign in. - if (!await _signInManager.CanSignInAsync(user)) - { - notification.Reject( - error: OpenIddictConstants.Errors.InvalidGrant, - description: "The user is no longer allowed to sign in."); - return; - } - - notification.Principal = await this.CreateClaimsPrincipalAsync(request, user); - } - } -} diff --git a/BTCPayServer/Security/OpenId/PasswordGrantTypeEventHandler.cs b/BTCPayServer/Security/OpenId/PasswordGrantTypeEventHandler.cs deleted file mode 100644 index b28018b4e..000000000 --- a/BTCPayServer/Security/OpenId/PasswordGrantTypeEventHandler.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Threading.Tasks; -using BTCPayServer.Data; -using BTCPayServer.Models; -using BTCPayServer.U2F; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Options; -using OpenIddict.Abstractions; -using OpenIddict.Core; -using OpenIddict.Server; -using Microsoft.AspNetCore; - -namespace BTCPayServer.Security.OpenId -{ - public class PasswordGrantTypeEventHandler : BaseOpenIdGrantHandler - { - private readonly UserManager _userManager; - private readonly NicolasDorier.RateLimits.RateLimitService _rateLimitService; - private readonly U2FService _u2FService; - - public PasswordGrantTypeEventHandler( - OpenIddictApplicationManager applicationManager, - OpenIddictAuthorizationManager authorizationManager, - SignInManager signInManager, - UserManager userManager, - NicolasDorier.RateLimits.RateLimitService rateLimitService, - IOptions identityOptions, U2FService u2FService) : base(applicationManager, - authorizationManager, signInManager, identityOptions) - { - _userManager = userManager; - _rateLimitService = rateLimitService; - _u2FService = u2FService; - } - - public static OpenIddictServerHandlerDescriptor Descriptor { get; } = - OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseScopedHandler() - .Build(); - - public override async ValueTask HandleAsync( - OpenIddictServerEvents.HandleTokenRequestContext notification) - { - var request = notification.Request; - if (!request.IsPasswordGrantType()) - { - return; - } - - var httpContext = notification.Transaction.GetHttpRequest().HttpContext; - await _rateLimitService.Throttle(ZoneLimits.Login, httpContext.Connection.RemoteIpAddress.ToString(), httpContext.RequestAborted); - 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.Reject( - error: OpenIddictConstants.Errors.InvalidGrant, - description: "The specified credentials are invalid."); - return; - } - - notification.Principal = await CreateClaimsPrincipalAsync(request, user); - } - } -} diff --git a/BTCPayServer/Security/OpenIdAuthorizationHandler.cs b/BTCPayServer/Security/OpenIdAuthorizationHandler.cs deleted file mode 100644 index a7413e41f..000000000 --- a/BTCPayServer/Security/OpenIdAuthorizationHandler.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using BTCPayServer.Data; -using BTCPayServer.Services.Stores; -using System.Security.Claims; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Primitives; -using static BTCPayServer.Security.OpenId.RestAPIPolicies; -using OpenIddict.Abstractions; -using BTCPayServer.Security.OpenId; - -namespace BTCPayServer.Security -{ - public class OpenIdAuthorizationHandler : AuthorizationHandler - { - private readonly HttpContext _HttpContext; - private readonly UserManager _userManager; - private readonly StoreRepository _storeRepository; - - public OpenIdAuthorizationHandler(IHttpContextAccessor httpContextAccessor, - UserManager userManager, - StoreRepository storeRepository) - { - _HttpContext = httpContextAccessor.HttpContext; - _userManager = userManager; - _storeRepository = storeRepository; - } - protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement) - { - if (context.User.Identity.AuthenticationType != "AuthenticationTypes.Federation") - return; - - bool success = false; - switch (requirement.Policy) - { - case Policies.CanModifyStoreSettings.Key: - if (!context.HasScopes(BTCPayScopes.StoreManagement)) - break; - // TODO: It should be possible to grant permission to a specific store - // we can do this by adding saving a claim with the specific store id - // to the access_token - string storeId = _HttpContext.GetImplicitStoreId(); - if (storeId == null) - { - success = true; - } - else - { - var userid = _userManager.GetUserId(context.User); - if (string.IsNullOrEmpty(userid)) - break; - var store = await _storeRepository.FindStore((string)storeId, userid); - if (store == null) - break; - success = true; - _HttpContext.SetStoreData(store); - } - break; - case Policies.CanModifyServerSettings.Key: - if (!context.HasScopes(BTCPayScopes.ServerManagement)) - break; - // For this authorization, we stil check in database because it is super sensitive. - var user = await _userManager.GetUserAsync(context.User); - if (user == null) - break; - if (!await _userManager.IsInRoleAsync(user, Roles.ServerAdmin)) - break; - success = true; - break; - } - - if (success) - { - context.Succeed(requirement); - } - } - } -} diff --git a/BTCPayServer/Security/SecurityExtensions.cs b/BTCPayServer/Security/SecurityExtensions.cs index 192aa3351..a90e39704 100644 --- a/BTCPayServer/Security/SecurityExtensions.cs +++ b/BTCPayServer/Security/SecurityExtensions.cs @@ -1,14 +1,8 @@ using System; -using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Routing; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; -using OpenIddict.Abstractions; -using BTCPayServer.Data; -using BTCPayServer.Services.Stores; -using Microsoft.AspNetCore.Identity; namespace BTCPayServer.Security { @@ -16,7 +10,7 @@ namespace BTCPayServer.Security { public static bool HasScopes(this AuthorizationHandlerContext context, params string[] scopes) { - return scopes.All(s => context.User.HasClaim(c => c.Type == OpenIddictConstants.Claims.Scope && c.Value.Split(' ').Contains(s))); + return scopes.All(s => context.User.HasClaim(c => c.Type.Equals("scope", StringComparison.InvariantCultureIgnoreCase) && c.Value.Split(' ').Contains(s))); } public static string GetImplicitStoreId(this HttpContext httpContext) { diff --git a/BTCPayServer/Views/Authorization/Authorize.cshtml b/BTCPayServer/Views/Authorization/Authorize.cshtml deleted file mode 100644 index 57085b8d9..000000000 --- a/BTCPayServer/Views/Authorization/Authorize.cshtml +++ /dev/null @@ -1,57 +0,0 @@ -@using BTCPayServer.Security.OpenId -@using OpenIddict.Abstractions -@model BTCPayServer.Models.Authorization.AuthorizeViewModel -@{ - - var scopeMappings = new Dictionary() - { - {BTCPayScopes.StoreManagement, ("Manage your stores", "The app will be able to create, modify and delete all your stores.")}, - {BTCPayScopes.ServerManagement, ("Manage your server", "The app will have total control on your server")}, - }; -} -
    - -
    -
    -
    -
    -

    Authorization Request

    -
    -

    @Model.ApplicationName is requesting access to your account.

    -
    -
    -
    -
    -
    - @foreach (var scope in Model.Scope) - { - @if (scopeMappings.TryGetValue(scope, out var text)) - { -
  • -
    @text.Title
    -

    @text.Description.

    -
  • - } - } -
    - -
    -
    -
    -
    -
    - - - -
    - - -
    -
    -
    -
    -
    From 023e64704d101ab08aeed72bf2feba682b5356ff Mon Sep 17 00:00:00 2001 From: Kukks Date: Tue, 18 Feb 2020 10:50:01 +0100 Subject: [PATCH 216/810] Add Swagger and Redoc Blocked by #1262 --- BTCPayServer/BTCPayServer.csproj | 1 + .../Controllers/ManageController.APIKeys.cs | 13 ++- .../Controllers/StoresController.BTCLike.cs | 2 +- BTCPayServer/Hosting/BTCPayServerServices.cs | 4 +- .../Hosting/OpenApi/IncludeInOpenApiDocs.cs | 9 ++ .../Hosting/OpenApi/OpenApiExtensions.cs | 96 +++++++++++++++++++ 6 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 BTCPayServer/Hosting/OpenApi/IncludeInOpenApiDocs.cs create mode 100644 BTCPayServer/Hosting/OpenApi/OpenApiExtensions.cs diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 749d9db8f..ae972afda 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -48,6 +48,7 @@ + diff --git a/BTCPayServer/Controllers/ManageController.APIKeys.cs b/BTCPayServer/Controllers/ManageController.APIKeys.cs index 26ff2aeec..03c78defe 100644 --- a/BTCPayServer/Controllers/ManageController.APIKeys.cs +++ b/BTCPayServer/Controllers/ManageController.APIKeys.cs @@ -4,13 +4,16 @@ using System.Globalization; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Data; +using BTCPayServer.Hosting.OpenApi; using BTCPayServer.Models; using BTCPayServer.Security; using BTCPayServer.Security.APIKeys; using ExchangeSharp; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; +using NSwag.Annotations; namespace BTCPayServer.Controllers { @@ -56,8 +59,16 @@ namespace BTCPayServer.Controllers return View("AddApiKey", await SetViewModelValues(new AddApiKeyViewModel())); } + /// The permissions to request. Current permissions available: ServerManagement, StoreManagement + /// The name of your application + /// If permissions are specified, and strict is set to false, it will allow the user to reject some of permissions the application is requesting. + /// If the application is requesting the CanModifyStoreSettings permission and selectiveStores is set to true, this allows the user to only grant permissions to selected stores under the user's control. [HttpGet("~/api-keys/authorize")] - public async Task AuthorizeAPIKey( string[] permissions, string applicationName = null, + [OpenApiTags("Authorization")] + [OpenApiOperation("Authorize User", + "Redirect the browser to this endpoint to request the user to generate an api-key with specific permissions")] + [IncludeInOpenApiDocs] + public async Task AuthorizeAPIKey(string[] permissions, string applicationName = null, bool strict = true, bool selectiveStores = false) { if (!_btcPayServerEnvironment.IsSecure) diff --git a/BTCPayServer/Controllers/StoresController.BTCLike.cs b/BTCPayServer/Controllers/StoresController.BTCLike.cs index 8b01c240f..d1da40d96 100644 --- a/BTCPayServer/Controllers/StoresController.BTCLike.cs +++ b/BTCPayServer/Controllers/StoresController.BTCLike.cs @@ -142,7 +142,7 @@ namespace BTCPayServer.Controllers [HttpPost] [Route("{storeId}/derivations/{cryptoCode}")] - public async Task AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, + public async Task AddDerivationScheme(string storeId, [FromBody] DerivationSchemeViewModel vm, string cryptoCode) { vm.CryptoCode = cryptoCode; diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 7072b63e9..d455efd65 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -26,6 +26,7 @@ using System.Threading; using BTCPayServer.Services.Wallets; using BTCPayServer.Logging; using BTCPayServer.HostedServices; +using BTCPayServer.Hosting.OpenApi; using BTCPayServer.PaymentRequest; using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; @@ -262,7 +263,7 @@ namespace BTCPayServer.Hosting } return rateLimits; }); - + services.AddBTCPayOpenApi(); services.AddLogging(logBuilder => { @@ -291,6 +292,7 @@ namespace BTCPayServer.Hosting public static IApplicationBuilder UsePayServer(this IApplicationBuilder app) { app.UseMiddleware(); + app.UseBTCPayOpenApi(); return app; } public static IApplicationBuilder UseHeadersOverride(this IApplicationBuilder app) diff --git a/BTCPayServer/Hosting/OpenApi/IncludeInOpenApiDocs.cs b/BTCPayServer/Hosting/OpenApi/IncludeInOpenApiDocs.cs new file mode 100644 index 000000000..5921efdaf --- /dev/null +++ b/BTCPayServer/Hosting/OpenApi/IncludeInOpenApiDocs.cs @@ -0,0 +1,9 @@ +using System; + + +namespace BTCPayServer.Hosting.OpenApi +{ + public class IncludeInOpenApiDocs : Attribute + { + } +} diff --git a/BTCPayServer/Hosting/OpenApi/OpenApiExtensions.cs b/BTCPayServer/Hosting/OpenApi/OpenApiExtensions.cs new file mode 100644 index 000000000..60c05330d --- /dev/null +++ b/BTCPayServer/Hosting/OpenApi/OpenApiExtensions.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using BTCPayServer.Payments; +using BTCPayServer.Security; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using NJsonSchema; +using NJsonSchema.Generation.TypeMappers; +using NSwag; +using NSwag.Generation.Processors.Security; + +namespace BTCPayServer.Hosting.OpenApi +{ + public static class OpenApiExtensions + { + public static IServiceCollection AddBTCPayOpenApi(this IServiceCollection serviceCollection) + { + + return serviceCollection.AddOpenApiDocument(config => + { + config.PostProcess = document => + { + document.Info.Version = "v1"; + document.Info.Title = "BTCPay Greenfield API"; + document.Info.Description = "A full API to use your BTCPay Server"; + document.Info.TermsOfService = null; + document.Info.Contact = new NSwag.OpenApiContact + { + Name = "BTCPay Server", Email = string.Empty, Url = "https://btcpayserver.org" + }; + }; + config.AddOperationFilter(context => + { + var methodInfo = context.MethodInfo; + if (methodInfo != null) + { + return methodInfo.CustomAttributes.Any(data => + data.AttributeType == typeof(IncludeInOpenApiDocs)) || + methodInfo.DeclaringType.CustomAttributes.Any(data => + data.AttributeType == typeof(IncludeInOpenApiDocs)); + } + + return false; + }); + + config.AddSecurity("APIKey", Enumerable.Empty(), + new OpenApiSecurityScheme + { + Type = OpenApiSecuritySchemeType.ApiKey, + Name = "Authorization", + In = OpenApiSecurityApiKeyLocation.Header, + Description = + "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: token {token}. For a smoother experience, you can generate a url that redirects users to an API key creation screen." + }); + + config.OperationProcessors.Add( + new BTCPayPolicyOperationProcessor("APIKey", AuthenticationSchemes.ApiKey)); + + config.TypeMappers.Add( + new PrimitiveTypeMapper(typeof(PaymentType), s => s.Type = JsonObjectType.String)); + config.TypeMappers.Add(new PrimitiveTypeMapper(typeof(PaymentMethodId), + s => s.Type = JsonObjectType.String)); + }); + } + + public static IApplicationBuilder UseBTCPayOpenApi(this IApplicationBuilder builder) + { + return builder.UseOpenApi() + .UseReDoc(settings => settings.Path = "/docs"); + } + + + class BTCPayPolicyOperationProcessor : AspNetCoreOperationSecurityScopeProcessor + { + private readonly string _authScheme; + + public BTCPayPolicyOperationProcessor(string x, string authScheme) : base(x) + { + _authScheme = authScheme; + } + + protected override IEnumerable GetScopes(IEnumerable authorizeAttributes) + { + var result = authorizeAttributes + .Where(attribute => attribute?.AuthenticationSchemes != null && attribute.Policy != null && + attribute.AuthenticationSchemes.Equals(_authScheme, + StringComparison.InvariantCultureIgnoreCase)) + .Select(attribute => attribute.Policy); + + return result; + } + } + } +} From 5cbe61e2e0e3d528e44006261e2b2069d86ebb1c Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 25 Feb 2020 17:21:08 +0900 Subject: [PATCH 217/810] Allow user to set the expirationTime of invoice via the API (Fix #1336) --- BTCPayServer/Controllers/InvoiceController.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index 8ef048379..8575df334 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -74,7 +74,11 @@ namespace BTCPayServer.Controllers var getAppsTaggingStore = _InvoiceRepository.GetAppsTaggingStore(store.Id); var storeBlob = store.GetStoreBlob(); EmailAddressAttribute emailValidator = new EmailAddressAttribute(); - entity.ExpirationTime = entity.InvoiceTime.AddMinutes(storeBlob.InvoiceExpiration); + entity.ExpirationTime = invoice.ExpirationTime is DateTimeOffset v ? v : entity.InvoiceTime.AddMinutes(storeBlob.InvoiceExpiration); + if (entity.ExpirationTime - TimeSpan.FromSeconds(30.0) < DateTimeOffset.UtcNow) + { + throw new BitpayHttpException(400, "The expirationTime is set too soon"); + } entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration); entity.OrderId = invoice.OrderId; entity.ServerUrl = serverUrl; From fec5637040da3f1486d42de6669b1f531699f498 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 25 Feb 2020 17:22:39 +0900 Subject: [PATCH 218/810] Replace Datetime.UTCNow by entity.InvoiceTime --- BTCPayServer/Controllers/InvoiceController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index 8575df334..118b5fdc6 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -75,7 +75,7 @@ namespace BTCPayServer.Controllers var storeBlob = store.GetStoreBlob(); EmailAddressAttribute emailValidator = new EmailAddressAttribute(); entity.ExpirationTime = invoice.ExpirationTime is DateTimeOffset v ? v : entity.InvoiceTime.AddMinutes(storeBlob.InvoiceExpiration); - if (entity.ExpirationTime - TimeSpan.FromSeconds(30.0) < DateTimeOffset.UtcNow) + if (entity.ExpirationTime - TimeSpan.FromSeconds(30.0) < entity.InvoiceTime) { throw new BitpayHttpException(400, "The expirationTime is set too soon"); } From 0a8b303c11837f03a808f3c3b1f8306b7ddb8f9e Mon Sep 17 00:00:00 2001 From: Kukks Date: Tue, 25 Feb 2020 14:43:53 +0100 Subject: [PATCH 219/810] add label for api keys, make api keys without -, fix null exception on authorize --- BTCPayServer.Data/Data/APIKeyData.cs | 1 + .../20200225133433_AddApiKeyLabel.Designer.cs | 849 ++++++++++++++++++ .../20200225133433_AddApiKeyLabel.cs | 22 + .../ApplicationDbContextModelSnapshot.cs | 3 + .../Controllers/ManageController.APIKeys.cs | 9 +- BTCPayServer/Views/Manage/APIKeys.cshtml | 6 +- BTCPayServer/Views/Manage/AddApiKey.cshtml | 8 + .../Views/Manage/AuthorizeAPIKey.cshtml | 8 + 8 files changed, 902 insertions(+), 4 deletions(-) create mode 100644 BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.Designer.cs create mode 100644 BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.cs diff --git a/BTCPayServer.Data/Data/APIKeyData.cs b/BTCPayServer.Data/Data/APIKeyData.cs index 715d3f2ec..c538a9bd9 100644 --- a/BTCPayServer.Data/Data/APIKeyData.cs +++ b/BTCPayServer.Data/Data/APIKeyData.cs @@ -26,6 +26,7 @@ namespace BTCPayServer.Data public StoreData StoreData { get; set; } public ApplicationUser User { get; set; } + public string Label { get; set; } public string[] GetPermissions() { return Permissions?.Split(';') ?? new string[0]; } public void SetPermissions(IEnumerable permissions) diff --git a/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.Designer.cs b/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.Designer.cs new file mode 100644 index 000000000..a73d3f0bc --- /dev/null +++ b/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.Designer.cs @@ -0,0 +1,849 @@ +// +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("20200225133433_AddApiKeyLabel")] + partial class AddApiKeyLabel + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.1"); + + modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasMaxLength(50); + + b.Property("Label") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("StoreId") + .HasColumnType("TEXT") + .HasMaxLength(50); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasMaxLength(50); + + b.HasKey("Id"); + + b.HasIndex("StoreId"); + + b.HasIndex("UserId"); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("CreatedTime") + .HasColumnType("TEXT"); + + b.Property("InvoiceDataId") + .HasColumnType("TEXT"); + + b.HasKey("Address"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("AddressInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.AppData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AppType") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.Property("StoreDataId") + .HasColumnType("TEXT"); + + b.Property("TagAllInvoices") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("StoreDataId"); + + b.ToTable("Apps"); + }); + + modelBuilder.Entity("BTCPayServer.Data.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RequiresEmailConfirmation") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => + { + b.Property("InvoiceDataId") + .HasColumnType("TEXT"); + + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("Assigned") + .HasColumnType("TEXT"); + + b.Property("CryptoCode") + .HasColumnType("TEXT"); + + b.Property("UnAssigned") + .HasColumnType("TEXT"); + + b.HasKey("InvoiceDataId", "Address"); + + b.ToTable("HistoricalAddressInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Blob") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CustomerEmail") + .HasColumnType("TEXT"); + + b.Property("ExceptionStatus") + .HasColumnType("TEXT"); + + b.Property("ItemCode") + .HasColumnType("TEXT"); + + b.Property("OrderId") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("TEXT"); + + b.Property("StoreDataId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("StoreDataId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b => + { + b.Property("InvoiceDataId") + .HasColumnType("TEXT"); + + b.Property("UniqueId") + .HasColumnType("TEXT"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.HasKey("InvoiceDataId", "UniqueId"); + + b.ToTable("InvoiceEvents"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Label") + .HasColumnType("TEXT"); + + b.Property("PairingTime") + .HasColumnType("TEXT"); + + b.Property("SIN") + .HasColumnType("TEXT"); + + b.Property("StoreDataId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SIN"); + + b.HasIndex("StoreDataId"); + + b.ToTable("PairedSINData"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("Expiration") + .HasColumnType("TEXT"); + + b.Property("Facade") + .HasColumnType("TEXT"); + + b.Property("Label") + .HasColumnType("TEXT"); + + b.Property("SIN") + .HasColumnType("TEXT"); + + b.Property("StoreDataId") + .HasColumnType("TEXT"); + + b.Property("TokenValue") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("PairingCodes"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Accounted") + .HasColumnType("INTEGER"); + + b.Property("Blob") + .HasColumnType("BLOB"); + + b.Property("InvoiceDataId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PaymentRequestData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Blob") + .HasColumnType("BLOB"); + + b.Property("Created") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(new DateTimeOffset(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0))); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("StoreDataId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("StoreDataId"); + + b.ToTable("PaymentRequests"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("PendingInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Blob") + .HasColumnType("BLOB"); + + b.Property("InvoiceDataId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("RefundAddresses"); + }); + + modelBuilder.Entity("BTCPayServer.Data.SettingData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("BTCPayServer.Data.StoreData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("DefaultCrypto") + .HasColumnType("TEXT"); + + b.Property("DerivationStrategies") + .HasColumnType("TEXT"); + + b.Property("DerivationStrategy") + .HasColumnType("TEXT"); + + b.Property("SpeedPolicy") + .HasColumnType("INTEGER"); + + b.Property("StoreBlob") + .HasColumnType("BLOB"); + + b.Property("StoreCertificate") + .HasColumnType("BLOB"); + + b.Property("StoreName") + .HasColumnType("TEXT"); + + b.Property("StoreWebsite") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Stores"); + }); + + modelBuilder.Entity("BTCPayServer.Data.StoredFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApplicationUserId") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("StorageFileName") + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationUserId") + .HasColumnType("TEXT"); + + b.Property("AttestationCert") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("Counter") + .HasColumnType("INTEGER"); + + b.Property("KeyHandle") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("U2FDevices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.UserStore", b => + { + b.Property("ApplicationUserId") + .HasColumnType("TEXT"); + + b.Property("StoreDataId") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.HasKey("ApplicationUserId", "StoreDataId"); + + b.HasIndex("StoreDataId"); + + b.ToTable("UserStore"); + }); + + modelBuilder.Entity("BTCPayServer.Data.WalletData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Blob") + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("Wallets"); + }); + + modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b => + { + b.Property("WalletDataId") + .HasColumnType("TEXT"); + + b.Property("TransactionId") + .HasColumnType("TEXT"); + + b.Property("Blob") + .HasColumnType("BLOB"); + + b.Property("Labels") + .HasColumnType("TEXT"); + + b.HasKey("WalletDataId", "TransactionId"); + + b.ToTable("WalletTransactions"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("APIKeys") + .HasForeignKey("StoreId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("BTCPayServer.Data.ApplicationUser", "User") + .WithMany("APIKeys") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("AddressInvoices") + .HasForeignKey("InvoiceDataId") + .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) + .IsRequired(); + }); + + 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) + .IsRequired(); + }); + + 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.PaymentRequestData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("PaymentRequests") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("PendingInvoices") + .HasForeignKey("Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("RefundAddresses") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.StoredFile", b => + { + b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") + .WithMany("StoredFiles") + .HasForeignKey("ApplicationUserId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b => + { + b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") + .WithMany("U2FDevices") + .HasForeignKey("ApplicationUserId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.UserStore", b => + { + b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") + .WithMany("UserStores") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("UserStores") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b => + { + b.HasOne("BTCPayServer.Data.WalletData", "WalletData") + .WithMany("WalletTransactions") + .HasForeignKey("WalletDataId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("BTCPayServer.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("BTCPayServer.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BTCPayServer.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("BTCPayServer.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.cs b/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.cs new file mode 100644 index 000000000..6db60f5c0 --- /dev/null +++ b/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace BTCPayServer.Migrations +{ + public partial class AddApiKeyLabel : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Label", + table: "ApiKeys", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Label", + table: "ApiKeys"); + } + } +} diff --git a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs index b2a26241a..3ee8f2a07 100644 --- a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -22,6 +22,9 @@ namespace BTCPayServer.Migrations .HasColumnType("TEXT") .HasMaxLength(50); + b.Property("Label") + .HasColumnType("TEXT"); + b.Property("Permissions") .HasColumnType("TEXT"); diff --git a/BTCPayServer/Controllers/ManageController.APIKeys.cs b/BTCPayServer/Controllers/ManageController.APIKeys.cs index 03c78defe..9be2bec0d 100644 --- a/BTCPayServer/Controllers/ManageController.APIKeys.cs +++ b/BTCPayServer/Controllers/ManageController.APIKeys.cs @@ -85,6 +85,7 @@ namespace BTCPayServer.Controllers var vm = await SetViewModelValues(new AuthorizeApiKeysViewModel() { + Label = applicationName, ServerManagementPermission = permissions.Contains(APIKeyConstants.Permissions.ServerManagement), StoreManagementPermission = permissions.Contains(APIKeyConstants.Permissions.StoreManagement), PermissionsFormatted = permissions, @@ -225,7 +226,10 @@ namespace BTCPayServer.Controllers { var key = new APIKeyData() { - Id = Guid.NewGuid().ToString(), Type = APIKeyType.Permanent, UserId = _userManager.GetUserId(User) + Id = Guid.NewGuid().ToString().Replace("-", string.Empty), + Type = APIKeyType.Permanent, + UserId = _userManager.GetUserId(User), + Label = viewModel.Label }; key.SetPermissions(GetPermissionsFromViewModel(viewModel)); await _apiKeyRepository.CreateKey(key); @@ -262,6 +266,7 @@ namespace BTCPayServer.Controllers public class AddApiKeyViewModel { + public string Label { get; set; } public StoreData[] Stores { get; set; } public ApiKeyStoreMode StoreMode { get; set; } public List SpecificStores { get; set; } = new List(); @@ -288,7 +293,7 @@ namespace BTCPayServer.Controllers { get { - return Permissions?.Split(";", StringSplitOptions.RemoveEmptyEntries); + return Permissions?.Split(";", StringSplitOptions.RemoveEmptyEntries)?? Array.Empty(); } set { diff --git a/BTCPayServer/Views/Manage/APIKeys.cshtml b/BTCPayServer/Views/Manage/APIKeys.cshtml index 23db9ac73..5c6656ef5 100644 --- a/BTCPayServer/Views/Manage/APIKeys.cshtml +++ b/BTCPayServer/Views/Manage/APIKeys.cshtml @@ -8,8 +8,9 @@ - - + + + @@ -17,6 +18,7 @@ @foreach (var keyData in Model.ApiKeyDatas) { + - } - From 95b9884af7c249307773f8f0bf2ffde9cbed4e34 Mon Sep 17 00:00:00 2001 From: Kukks Date: Wed, 26 Feb 2020 09:41:24 +0100 Subject: [PATCH 227/810] Revert "consolidate migrations" This reverts commit 501c3241b543c5941ec9e5d7ecc14cf1edf9661f. --- .../20200119130108_ExtendApiKeys.cs | 10 - .../20200225133433_AddApiKeyLabel.Designer.cs | 849 ++++++++++++++++++ .../20200225133433_AddApiKeyLabel.cs | 22 + 3 files changed, 871 insertions(+), 10 deletions(-) create mode 100644 BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.Designer.cs create mode 100644 BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.cs diff --git a/BTCPayServer.Data/Migrations/20200119130108_ExtendApiKeys.cs b/BTCPayServer.Data/Migrations/20200119130108_ExtendApiKeys.cs index 05fa236cf..9eeb5b83c 100644 --- a/BTCPayServer.Data/Migrations/20200119130108_ExtendApiKeys.cs +++ b/BTCPayServer.Data/Migrations/20200119130108_ExtendApiKeys.cs @@ -26,11 +26,6 @@ namespace BTCPayServer.Migrations table: "ApiKeys", maxLength: 50, nullable: true); - - migrationBuilder.AddColumn( - name: "Label", - table: "ApiKeys", - nullable: true); migrationBuilder.CreateIndex( name: "IX_ApiKeys_UserId", @@ -73,11 +68,6 @@ namespace BTCPayServer.Migrations migrationBuilder.DropColumn( name: "UserId", table: "ApiKeys"); - - migrationBuilder.DropColumn( - name: "Label", - table: "ApiKeys"); - } } } diff --git a/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.Designer.cs b/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.Designer.cs new file mode 100644 index 000000000..a73d3f0bc --- /dev/null +++ b/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.Designer.cs @@ -0,0 +1,849 @@ +// +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("20200225133433_AddApiKeyLabel")] + partial class AddApiKeyLabel + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.1"); + + modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasMaxLength(50); + + b.Property("Label") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("StoreId") + .HasColumnType("TEXT") + .HasMaxLength(50); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasMaxLength(50); + + b.HasKey("Id"); + + b.HasIndex("StoreId"); + + b.HasIndex("UserId"); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("CreatedTime") + .HasColumnType("TEXT"); + + b.Property("InvoiceDataId") + .HasColumnType("TEXT"); + + b.HasKey("Address"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("AddressInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.AppData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AppType") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.Property("StoreDataId") + .HasColumnType("TEXT"); + + b.Property("TagAllInvoices") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("StoreDataId"); + + b.ToTable("Apps"); + }); + + modelBuilder.Entity("BTCPayServer.Data.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RequiresEmailConfirmation") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => + { + b.Property("InvoiceDataId") + .HasColumnType("TEXT"); + + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("Assigned") + .HasColumnType("TEXT"); + + b.Property("CryptoCode") + .HasColumnType("TEXT"); + + b.Property("UnAssigned") + .HasColumnType("TEXT"); + + b.HasKey("InvoiceDataId", "Address"); + + b.ToTable("HistoricalAddressInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Blob") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CustomerEmail") + .HasColumnType("TEXT"); + + b.Property("ExceptionStatus") + .HasColumnType("TEXT"); + + b.Property("ItemCode") + .HasColumnType("TEXT"); + + b.Property("OrderId") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("TEXT"); + + b.Property("StoreDataId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("StoreDataId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b => + { + b.Property("InvoiceDataId") + .HasColumnType("TEXT"); + + b.Property("UniqueId") + .HasColumnType("TEXT"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.HasKey("InvoiceDataId", "UniqueId"); + + b.ToTable("InvoiceEvents"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Label") + .HasColumnType("TEXT"); + + b.Property("PairingTime") + .HasColumnType("TEXT"); + + b.Property("SIN") + .HasColumnType("TEXT"); + + b.Property("StoreDataId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SIN"); + + b.HasIndex("StoreDataId"); + + b.ToTable("PairedSINData"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("Expiration") + .HasColumnType("TEXT"); + + b.Property("Facade") + .HasColumnType("TEXT"); + + b.Property("Label") + .HasColumnType("TEXT"); + + b.Property("SIN") + .HasColumnType("TEXT"); + + b.Property("StoreDataId") + .HasColumnType("TEXT"); + + b.Property("TokenValue") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("PairingCodes"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Accounted") + .HasColumnType("INTEGER"); + + b.Property("Blob") + .HasColumnType("BLOB"); + + b.Property("InvoiceDataId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PaymentRequestData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Blob") + .HasColumnType("BLOB"); + + b.Property("Created") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(new DateTimeOffset(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0))); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("StoreDataId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("StoreDataId"); + + b.ToTable("PaymentRequests"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("PendingInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Blob") + .HasColumnType("BLOB"); + + b.Property("InvoiceDataId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("RefundAddresses"); + }); + + modelBuilder.Entity("BTCPayServer.Data.SettingData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("BTCPayServer.Data.StoreData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("DefaultCrypto") + .HasColumnType("TEXT"); + + b.Property("DerivationStrategies") + .HasColumnType("TEXT"); + + b.Property("DerivationStrategy") + .HasColumnType("TEXT"); + + b.Property("SpeedPolicy") + .HasColumnType("INTEGER"); + + b.Property("StoreBlob") + .HasColumnType("BLOB"); + + b.Property("StoreCertificate") + .HasColumnType("BLOB"); + + b.Property("StoreName") + .HasColumnType("TEXT"); + + b.Property("StoreWebsite") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Stores"); + }); + + modelBuilder.Entity("BTCPayServer.Data.StoredFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApplicationUserId") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("StorageFileName") + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationUserId") + .HasColumnType("TEXT"); + + b.Property("AttestationCert") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("Counter") + .HasColumnType("INTEGER"); + + b.Property("KeyHandle") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("U2FDevices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.UserStore", b => + { + b.Property("ApplicationUserId") + .HasColumnType("TEXT"); + + b.Property("StoreDataId") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.HasKey("ApplicationUserId", "StoreDataId"); + + b.HasIndex("StoreDataId"); + + b.ToTable("UserStore"); + }); + + modelBuilder.Entity("BTCPayServer.Data.WalletData", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Blob") + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("Wallets"); + }); + + modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b => + { + b.Property("WalletDataId") + .HasColumnType("TEXT"); + + b.Property("TransactionId") + .HasColumnType("TEXT"); + + b.Property("Blob") + .HasColumnType("BLOB"); + + b.Property("Labels") + .HasColumnType("TEXT"); + + b.HasKey("WalletDataId", "TransactionId"); + + b.ToTable("WalletTransactions"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("APIKeys") + .HasForeignKey("StoreId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("BTCPayServer.Data.ApplicationUser", "User") + .WithMany("APIKeys") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("AddressInvoices") + .HasForeignKey("InvoiceDataId") + .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) + .IsRequired(); + }); + + 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) + .IsRequired(); + }); + + 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.PaymentRequestData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("PaymentRequests") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("PendingInvoices") + .HasForeignKey("Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("RefundAddresses") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.StoredFile", b => + { + b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") + .WithMany("StoredFiles") + .HasForeignKey("ApplicationUserId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b => + { + b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") + .WithMany("U2FDevices") + .HasForeignKey("ApplicationUserId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.UserStore", b => + { + b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") + .WithMany("UserStores") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("UserStores") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b => + { + b.HasOne("BTCPayServer.Data.WalletData", "WalletData") + .WithMany("WalletTransactions") + .HasForeignKey("WalletDataId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("BTCPayServer.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("BTCPayServer.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BTCPayServer.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("BTCPayServer.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.cs b/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.cs new file mode 100644 index 000000000..6db60f5c0 --- /dev/null +++ b/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace BTCPayServer.Migrations +{ + public partial class AddApiKeyLabel : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Label", + table: "ApiKeys", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Label", + table: "ApiKeys"); + } + } +} From 48c21baee59aaeaa7e5083d5794b9acb6d57ba3b Mon Sep 17 00:00:00 2001 From: Kukks Date: Wed, 26 Feb 2020 09:53:58 +0100 Subject: [PATCH 228/810] add migration attributes and remove designer --- .../20200224134444_Remove_OpenIddict.cs | 4 + .../20200225133433_AddApiKeyLabel.Designer.cs | 849 ------------------ .../20200225133433_AddApiKeyLabel.cs | 7 +- 3 files changed, 10 insertions(+), 850 deletions(-) delete mode 100644 BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.Designer.cs diff --git a/BTCPayServer.Data/Migrations/20200224134444_Remove_OpenIddict.cs b/BTCPayServer.Data/Migrations/20200224134444_Remove_OpenIddict.cs index 78f323529..d356af760 100644 --- a/BTCPayServer.Data/Migrations/20200224134444_Remove_OpenIddict.cs +++ b/BTCPayServer.Data/Migrations/20200224134444_Remove_OpenIddict.cs @@ -1,8 +1,12 @@ using System; +using BTCPayServer.Data; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; namespace BTCPayServer.Migrations { + [DbContext(typeof(ApplicationDbContext))] + [Migration("20200224134444_Remove_OpenIddict")] public partial class Remove_OpenIddict : Migration { protected override void Up(MigrationBuilder migrationBuilder) diff --git a/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.Designer.cs b/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.Designer.cs deleted file mode 100644 index a73d3f0bc..000000000 --- a/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.Designer.cs +++ /dev/null @@ -1,849 +0,0 @@ -// -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("20200225133433_AddApiKeyLabel")] - partial class AddApiKeyLabel - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.1"); - - modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => - { - b.Property("Id") - .HasColumnType("TEXT") - .HasMaxLength(50); - - b.Property("Label") - .HasColumnType("TEXT"); - - b.Property("Permissions") - .HasColumnType("TEXT"); - - b.Property("StoreId") - .HasColumnType("TEXT") - .HasMaxLength(50); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("TEXT") - .HasMaxLength(50); - - b.HasKey("Id"); - - b.HasIndex("StoreId"); - - b.HasIndex("UserId"); - - b.ToTable("ApiKeys"); - }); - - modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => - { - b.Property("Address") - .HasColumnType("TEXT"); - - b.Property("CreatedTime") - .HasColumnType("TEXT"); - - b.Property("InvoiceDataId") - .HasColumnType("TEXT"); - - b.HasKey("Address"); - - b.HasIndex("InvoiceDataId"); - - b.ToTable("AddressInvoices"); - }); - - modelBuilder.Entity("BTCPayServer.Data.AppData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AppType") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Settings") - .HasColumnType("TEXT"); - - b.Property("StoreDataId") - .HasColumnType("TEXT"); - - b.Property("TagAllInvoices") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("StoreDataId"); - - b.ToTable("Apps"); - }); - - modelBuilder.Entity("BTCPayServer.Data.ApplicationUser", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Email") - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.Property("NormalizedUserName") - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RequiresEmailConfirmation") - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasName("UserNameIndex"); - - b.ToTable("AspNetUsers"); - }); - - modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => - { - b.Property("InvoiceDataId") - .HasColumnType("TEXT"); - - b.Property("Address") - .HasColumnType("TEXT"); - - b.Property("Assigned") - .HasColumnType("TEXT"); - - b.Property("CryptoCode") - .HasColumnType("TEXT"); - - b.Property("UnAssigned") - .HasColumnType("TEXT"); - - b.HasKey("InvoiceDataId", "Address"); - - b.ToTable("HistoricalAddressInvoices"); - }); - - modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Blob") - .HasColumnType("BLOB"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CustomerEmail") - .HasColumnType("TEXT"); - - b.Property("ExceptionStatus") - .HasColumnType("TEXT"); - - b.Property("ItemCode") - .HasColumnType("TEXT"); - - b.Property("OrderId") - .HasColumnType("TEXT"); - - b.Property("Status") - .HasColumnType("TEXT"); - - b.Property("StoreDataId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("StoreDataId"); - - b.ToTable("Invoices"); - }); - - modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b => - { - b.Property("InvoiceDataId") - .HasColumnType("TEXT"); - - b.Property("UniqueId") - .HasColumnType("TEXT"); - - b.Property("Message") - .HasColumnType("TEXT"); - - b.Property("Timestamp") - .HasColumnType("TEXT"); - - b.HasKey("InvoiceDataId", "UniqueId"); - - b.ToTable("InvoiceEvents"); - }); - - modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Label") - .HasColumnType("TEXT"); - - b.Property("PairingTime") - .HasColumnType("TEXT"); - - b.Property("SIN") - .HasColumnType("TEXT"); - - b.Property("StoreDataId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SIN"); - - b.HasIndex("StoreDataId"); - - b.ToTable("PairedSINData"); - }); - - modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("DateCreated") - .HasColumnType("TEXT"); - - b.Property("Expiration") - .HasColumnType("TEXT"); - - b.Property("Facade") - .HasColumnType("TEXT"); - - b.Property("Label") - .HasColumnType("TEXT"); - - b.Property("SIN") - .HasColumnType("TEXT"); - - b.Property("StoreDataId") - .HasColumnType("TEXT"); - - b.Property("TokenValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("PairingCodes"); - }); - - modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Accounted") - .HasColumnType("INTEGER"); - - b.Property("Blob") - .HasColumnType("BLOB"); - - b.Property("InvoiceDataId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("InvoiceDataId"); - - b.ToTable("Payments"); - }); - - modelBuilder.Entity("BTCPayServer.Data.PaymentRequestData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Blob") - .HasColumnType("BLOB"); - - b.Property("Created") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(new DateTimeOffset(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0))); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.Property("StoreDataId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Status"); - - b.HasIndex("StoreDataId"); - - b.ToTable("PaymentRequests"); - }); - - modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("PendingInvoices"); - }); - - modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Blob") - .HasColumnType("BLOB"); - - b.Property("InvoiceDataId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("InvoiceDataId"); - - b.ToTable("RefundAddresses"); - }); - - modelBuilder.Entity("BTCPayServer.Data.SettingData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); - - modelBuilder.Entity("BTCPayServer.Data.StoreData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("DefaultCrypto") - .HasColumnType("TEXT"); - - b.Property("DerivationStrategies") - .HasColumnType("TEXT"); - - b.Property("DerivationStrategy") - .HasColumnType("TEXT"); - - b.Property("SpeedPolicy") - .HasColumnType("INTEGER"); - - b.Property("StoreBlob") - .HasColumnType("BLOB"); - - b.Property("StoreCertificate") - .HasColumnType("BLOB"); - - b.Property("StoreName") - .HasColumnType("TEXT"); - - b.Property("StoreWebsite") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Stores"); - }); - - modelBuilder.Entity("BTCPayServer.Data.StoredFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("ApplicationUserId") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("StorageFileName") - .HasColumnType("TEXT"); - - b.Property("Timestamp") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationUserId"); - - b.ToTable("Files"); - }); - - modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ApplicationUserId") - .HasColumnType("TEXT"); - - b.Property("AttestationCert") - .IsRequired() - .HasColumnType("BLOB"); - - b.Property("Counter") - .HasColumnType("INTEGER"); - - b.Property("KeyHandle") - .IsRequired() - .HasColumnType("BLOB"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PublicKey") - .IsRequired() - .HasColumnType("BLOB"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationUserId"); - - b.ToTable("U2FDevices"); - }); - - modelBuilder.Entity("BTCPayServer.Data.UserStore", b => - { - b.Property("ApplicationUserId") - .HasColumnType("TEXT"); - - b.Property("StoreDataId") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("TEXT"); - - b.HasKey("ApplicationUserId", "StoreDataId"); - - b.HasIndex("StoreDataId"); - - b.ToTable("UserStore"); - }); - - modelBuilder.Entity("BTCPayServer.Data.WalletData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Blob") - .HasColumnType("BLOB"); - - b.HasKey("Id"); - - b.ToTable("Wallets"); - }); - - modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b => - { - b.Property("WalletDataId") - .HasColumnType("TEXT"); - - b.Property("TransactionId") - .HasColumnType("TEXT"); - - b.Property("Blob") - .HasColumnType("BLOB"); - - b.Property("Labels") - .HasColumnType("TEXT"); - - b.HasKey("WalletDataId", "TransactionId"); - - b.ToTable("WalletTransactions"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.Property("NormalizedName") - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasName("RoleNameIndex"); - - b.ToTable("AspNetRoles"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens"); - }); - - modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => - { - b.HasOne("BTCPayServer.Data.StoreData", "StoreData") - .WithMany("APIKeys") - .HasForeignKey("StoreId") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("BTCPayServer.Data.ApplicationUser", "User") - .WithMany("APIKeys") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => - { - b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") - .WithMany("AddressInvoices") - .HasForeignKey("InvoiceDataId") - .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) - .IsRequired(); - }); - - 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) - .IsRequired(); - }); - - 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.PaymentRequestData", b => - { - b.HasOne("BTCPayServer.Data.StoreData", "StoreData") - .WithMany("PaymentRequests") - .HasForeignKey("StoreDataId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => - { - b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") - .WithMany("PendingInvoices") - .HasForeignKey("Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => - { - b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") - .WithMany("RefundAddresses") - .HasForeignKey("InvoiceDataId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("BTCPayServer.Data.StoredFile", b => - { - b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") - .WithMany("StoredFiles") - .HasForeignKey("ApplicationUserId"); - }); - - modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b => - { - b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") - .WithMany("U2FDevices") - .HasForeignKey("ApplicationUserId"); - }); - - modelBuilder.Entity("BTCPayServer.Data.UserStore", b => - { - b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") - .WithMany("UserStores") - .HasForeignKey("ApplicationUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("BTCPayServer.Data.StoreData", "StoreData") - .WithMany("UserStores") - .HasForeignKey("StoreDataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b => - { - b.HasOne("BTCPayServer.Data.WalletData", "WalletData") - .WithMany("WalletTransactions") - .HasForeignKey("WalletDataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("BTCPayServer.Data.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("BTCPayServer.Data.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("BTCPayServer.Data.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("BTCPayServer.Data.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.cs b/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.cs index 6db60f5c0..332c7a48f 100644 --- a/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.cs +++ b/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.cs @@ -1,9 +1,14 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using BTCPayServer.Data; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; namespace BTCPayServer.Migrations { + [DbContext(typeof(ApplicationDbContext))] + [Migration("20200225133433_AddApiKeyLabel")] public partial class AddApiKeyLabel : Migration { + protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddColumn( From e7eea1036bf8ef36b4d7e39b72dac1d08b241971 Mon Sep 17 00:00:00 2001 From: Kukks Date: Wed, 26 Feb 2020 10:26:38 +0100 Subject: [PATCH 229/810] make api key delete use confirm page --- .../Controllers/ManageController.APIKeys.cs | 29 ++++++++++++++++--- BTCPayServer/Models/ConfirmModel.cs | 1 + BTCPayServer/Views/Manage/APIKeys.cshtml | 2 +- BTCPayServer/Views/Shared/Confirm.cshtml | 2 +- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/BTCPayServer/Controllers/ManageController.APIKeys.cs b/BTCPayServer/Controllers/ManageController.APIKeys.cs index 9be2bec0d..23052fe9b 100644 --- a/BTCPayServer/Controllers/ManageController.APIKeys.cs +++ b/BTCPayServer/Controllers/ManageController.APIKeys.cs @@ -8,11 +8,8 @@ using BTCPayServer.Hosting.OpenApi; using BTCPayServer.Models; using BTCPayServer.Security; using BTCPayServer.Security.APIKeys; -using ExchangeSharp; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json; using NSwag.Annotations; namespace BTCPayServer.Controllers @@ -31,9 +28,33 @@ namespace BTCPayServer.Controllers }); } - [HttpGet] + + + [HttpGet("api-keys/{id}/delete")] public async Task RemoveAPIKey(string id) { + var key = await _apiKeyRepository.GetKey(id); + if (key == null || key.UserId != _userManager.GetUserId(User)) + { + return NotFound(); + } + return View("Confirm", new ConfirmModel() + { + Title = "Delete API Key "+ ( string.IsNullOrEmpty(key.Label)? string.Empty: key.Label) + "("+key.Id+")", + Description = "Any application using this api key will immediately lose access", + Action = "Delete", + ActionUrl = Request.GetCurrentUrl().Replace("RemoveAPIKey", "RemoveAPIKeyPost") + }); + } + + [HttpPost("api-keys/{id}/delete")] + public async Task RemoveAPIKeyPost(string id) + { + var key = await _apiKeyRepository.GetKey(id); + if (key == null || key.UserId != _userManager.GetUserId(User)) + { + return NotFound(); + } await _apiKeyRepository.Remove(id, _userManager.GetUserId(User)); TempData.SetStatusMessageModel(new StatusMessageModel() { diff --git a/BTCPayServer/Models/ConfirmModel.cs b/BTCPayServer/Models/ConfirmModel.cs index b56f4274a..65caf78f1 100644 --- a/BTCPayServer/Models/ConfirmModel.cs +++ b/BTCPayServer/Models/ConfirmModel.cs @@ -29,5 +29,6 @@ namespace BTCPayServer.Models get; set; } public string ButtonClass { get; set; } = "btn-danger"; + public string ActionUrl { get; set; } } } diff --git a/BTCPayServer/Views/Manage/APIKeys.cshtml b/BTCPayServer/Views/Manage/APIKeys.cshtml index 4e4a69cca..ac197f44d 100644 --- a/BTCPayServer/Views/Manage/APIKeys.cshtml +++ b/BTCPayServer/Views/Manage/APIKeys.cshtml @@ -31,7 +31,7 @@ } } diff --git a/BTCPayServer/Views/Shared/Confirm.cshtml b/BTCPayServer/Views/Shared/Confirm.cshtml index 22f2975cb..d44105ae3 100644 --- a/BTCPayServer/Views/Shared/Confirm.cshtml +++ b/BTCPayServer/Views/Shared/Confirm.cshtml @@ -26,7 +26,7 @@ {
    -
    + From 9eac33793a0b00819ad86138dc033d99ecd83427 Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 24 Feb 2020 18:43:28 +0100 Subject: [PATCH 230/810] GreenField API #1: Get current API Key info --- BTCPayServer.Tests/GreenfieldAPITests.cs | 71 +++++++++++++++++++ .../Controllers/RestApi/ApiKeys/ApiKeyData.cs | 23 ++++++ .../RestApi/ApiKeys/ApiKeysController.cs | 37 ++++++++++ 3 files changed, 131 insertions(+) create mode 100644 BTCPayServer.Tests/GreenfieldAPITests.cs create mode 100644 BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeyData.cs create mode 100644 BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs new file mode 100644 index 000000000..d2ba99676 --- /dev/null +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -0,0 +1,71 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using BTCPayServer.Controllers; +using BTCPayServer.Controllers.RestApi.ApiKeys; +using BTCPayServer.Data; +using BTCPayServer.Security.APIKeys; +using BTCPayServer.Tests.Logging; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json.Linq; +using Xunit; +using Xunit.Abstractions; + +namespace BTCPayServer.Tests +{ + public class GreenfieldAPITests + { + public const int TestTimeout = TestUtils.TestTimeout; + + public const string TestApiPath = "api/test/apikey"; + + public GreenfieldAPITests(ITestOutputHelper helper) + { + Logs.Tester = new XUnitLog(helper) {Name = "Tests"}; + Logs.LogProvider = new XUnitLogProvider(helper); + } + + [Fact] + [Trait("Integration", "Integration")] + public async Task ApiKeysControllerTests() + { + using (var tester = ServerTester.Create()) + { + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + await user.MakeAdmin(); + string apiKey = await GenerateAPIKey(tester, user); + + //Get current api key + var request = new HttpRequestMessage(HttpMethod.Get, "api/v1/api-keys/current"); + request.Headers.Authorization = new AuthenticationHeaderValue("token", apiKey); + var result = await tester.PayTester.HttpClient.SendAsync(request); + Assert.True(result.IsSuccessStatusCode); + var apiKeyData = JObject.Parse(await result.Content.ReadAsStringAsync()).ToObject(); + Assert.NotNull(apiKeyData); + Assert.Equal(apiKey, apiKeyData.ApiKey); + Assert.Equal(user.UserId, apiKeyData.UserId); + Assert.Equal(2, apiKeyData.Permissions.Length); + } + } + + private static async Task GenerateAPIKey(ServerTester tester, TestAccount user) + { + var manageController = tester.PayTester.GetController(user.UserId, user.StoreId, user.IsAdmin); + var x = Assert.IsType(await manageController.AddApiKey( + new ManageController.AddApiKeyViewModel() + { + ServerManagementPermission = true, + StoreManagementPermission = true, + StoreMode = ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores + })); + var statusMessage = manageController.TempData.GetStatusMessageModel(); + Assert.NotNull(statusMessage); + var apiKey = statusMessage.Html.Substring(statusMessage.Html.IndexOf("") + 6); + apiKey = apiKey.Substring(0, apiKey.IndexOf("") ); + return apiKey; + } + } +} diff --git a/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeyData.cs b/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeyData.cs new file mode 100644 index 000000000..b308976c3 --- /dev/null +++ b/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeyData.cs @@ -0,0 +1,23 @@ +using BTCPayServer.Data; + +namespace BTCPayServer.Controllers.RestApi.ApiKeys +{ + public class ApiKeyData + { + public string ApiKey { get; set; } + public string Label { get; set; } + public string UserId { get; set; } + public string[] Permissions { get; set; } + + public static ApiKeyData FromModel(APIKeyData data) + { + return new ApiKeyData() + { + Permissions = data.GetPermissions(), + ApiKey = data.Id, + UserId = data.UserId, + Label = data.Label + }; + } + } +} diff --git a/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs b/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs new file mode 100644 index 000000000..ed45d77d6 --- /dev/null +++ b/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using BTCPayServer.Hosting.OpenApi; +using BTCPayServer.Security; +using BTCPayServer.Security.APIKeys; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; + +namespace BTCPayServer.Controllers.RestApi.ApiKeys +{ + [ApiController] + [IncludeInOpenApiDocs] + [OpenApiTags("API Keys")] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + public class ApiKeysController : ControllerBase + { + private readonly APIKeyRepository _apiKeyRepository; + + public ApiKeysController(APIKeyRepository apiKeyRepository) + { + _apiKeyRepository = apiKeyRepository; + } + + [OpenApiOperation("Get current API Key information", "View information about the current API key")] + [SwaggerResponse(StatusCodes.Status200OK, typeof(ApiKeyData), + Description = "Information about the current api key")] + [HttpGet("~/api/v1/api-keys/current")] + [HttpGet("~/api/v1/users/me/api-keys/current")] + public async Task> GetKey() + { + ControllerContext.HttpContext.GetAPIKey(out var apiKey); + var data = await _apiKeyRepository.GetKey(apiKey); + return Ok(ApiKeyData.FromModel(data)); + } + } +} From c60769623054cec3fb94293aaebbc6aa98ffb069 Mon Sep 17 00:00:00 2001 From: Kukks Date: Fri, 28 Feb 2020 12:51:15 +0100 Subject: [PATCH 231/810] Use proper divisibility for payments in crowdfund and do not show tooltip if identical data fixes #1037 and fixes #1003 --- .../AppViewModels/ViewCrowdfundViewModel.cs | 1 + BTCPayServer/Services/Apps/AppService.cs | 4 +++ .../AppsPublic/Crowdfund/VueCrowdfund.cshtml | 2 +- BTCPayServer/wwwroot/crowdfund/app.js | 33 ++++++++++--------- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/BTCPayServer/Models/AppViewModels/ViewCrowdfundViewModel.cs b/BTCPayServer/Models/AppViewModels/ViewCrowdfundViewModel.cs index 8ad197977..8fa7a5964 100644 --- a/BTCPayServer/Models/AppViewModels/ViewCrowdfundViewModel.cs +++ b/BTCPayServer/Models/AppViewModels/ViewCrowdfundViewModel.cs @@ -75,6 +75,7 @@ namespace BTCPayServer.Models.AppViewModels public bool DisplayPerksRanking { get; set; } public bool Enabled { get; set; } public string ResetEvery { get; set; } + public Dictionary CurrencyDataPayments { get; set; } } public class ContributeToCrowdfund diff --git a/BTCPayServer/Services/Apps/AppService.cs b/BTCPayServer/Services/Apps/AppService.cs index e824fdcfd..75e28a6e4 100644 --- a/BTCPayServer/Services/Apps/AppService.cs +++ b/BTCPayServer/Services/Apps/AppService.cs @@ -153,6 +153,10 @@ namespace BTCPayServer.Services.Apps Sounds = settings.Sounds, AnimationColors = settings.AnimationColors, CurrencyData = _Currencies.GetCurrencyData(settings.TargetCurrency, true), + CurrencyDataPayments = currentPayments.Select(pair => pair.Key) + .Concat(pendingPayments.Select(pair => pair.Key)).Distinct() + .Select(id => _Currencies.GetCurrencyData(id.CryptoCode, true)) + .ToDictionary(data => data.Code, data => data), Info = new ViewCrowdfundViewModel.CrowdfundInfo() { TotalContributors = paidInvoices.Length, diff --git a/BTCPayServer/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml b/BTCPayServer/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml index bff5e07fb..ade4f0c3a 100644 --- a/BTCPayServer/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml +++ b/BTCPayServer/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml @@ -128,7 +128,7 @@
    • - {{stat.label}} {{stat.value.toFixed(srvModel.currencyData.divisibility)}} + {{stat.label}} {{stat.value}}
    diff --git a/BTCPayServer/wwwroot/crowdfund/app.js b/BTCPayServer/wwwroot/crowdfund/app.js index 7d9e3242d..60f8dc9f8 100644 --- a/BTCPayServer/wwwroot/crowdfund/app.js +++ b/BTCPayServer/wwwroot/crowdfund/app.js @@ -109,11 +109,8 @@ addLoadEvent(function (ev) { return this.srvModel.targetCurrency.toUpperCase(); }, paymentStats: function(){ - var result= []; - + var result= []; var combinedStats = {}; - - var keys = Object.keys(this.srvModel.info.paymentStats); for (var i = 0; i < keys.length; i++) { @@ -135,20 +132,24 @@ addLoadEvent(function (ev) { } keys = Object.keys(combinedStats); - + for (var i = 0; i < keys.length; i++) { - var newItem = {key:keys[i], value: combinedStats[keys[i]], label: keys[i].replace("_","")}; - result.push(newItem); - - } - for (var i = 0; i < result.length; i++) { - var current = result[i]; - if(current.label.endsWith("LightningLike")){ - current.label = current.label.substr(0,current.label.indexOf("LightningLike")); - current.lightning = true; + if(!combinedStats[keys[i]]){ + continue; } + var paymentMethodId = keys[i].split("_"); + var value = combinedStats[keys[i]].toFixed(this.srvModel.currencyDataPayments[paymentMethodId[0]].divisibility); + var newItem = {key:keys[i], value: value, label: paymentMethodId[0]}; + + if(paymentMethodId.length > 1 && paymentMethodId[1].endsWith("LightningLike")){ + newItem.lightning = true; + } + result.push(newItem); + } + + if(result.length === 1 && result[0].label === srvModel.targetCurrency){ + return []; } - return result; }, perks: function(){ @@ -255,7 +256,7 @@ addLoadEvent(function (ev) { } ); }); eventAggregator.$on("payment-received", function (amount, cryptoCode, type) { - var onChain = type.toLowerCase() === "btclike"; + var onChain = type.toLowerCase() !== "lightninglike"; if(self.sound) { playRandomSound(); } From c3bfce76560faab5c5054eff616ae57a16a666a9 Mon Sep 17 00:00:00 2001 From: Kukks Date: Fri, 28 Feb 2020 16:01:44 +0100 Subject: [PATCH 232/810] Modal option for Pay Button closes #796 --- BTCPayServer/Controllers/PublicController.cs | 10 +++++- .../StoreViewModels/PayButtonViewModel.cs | 2 ++ BTCPayServer/Views/Stores/PayButton.cshtml | 5 +++ BTCPayServer/wwwroot/paybutton/paybutton.js | 34 +++++++++++++++++-- 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/BTCPayServer/Controllers/PublicController.cs b/BTCPayServer/Controllers/PublicController.cs index 5c0dea090..c30259ccb 100644 --- a/BTCPayServer/Controllers/PublicController.cs +++ b/BTCPayServer/Controllers/PublicController.cs @@ -37,7 +37,6 @@ namespace BTCPayServer.Controllers [HttpPost] [Route("api/v1/invoices")] - [MediaTypeAcceptConstraintAttribute("text/html")] [IgnoreAntiforgeryToken] [EnableCors(CorsPolicies.All)] public async Task PayButtonHandle([FromForm]PayButtonViewModel model, CancellationToken cancellationToken) @@ -78,6 +77,15 @@ namespace BTCPayServer.Controllers ModelState.AddModelError("Store", e.Message); return View(); } + + if (model.JsonResponse) + { + return Json(new + { + InvoiceId = invoice.Data.Id, + InvoiceUrl = invoice.Data.Url + }); + } if (string.IsNullOrEmpty(model.CheckoutQueryString)) { diff --git a/BTCPayServer/Models/StoreViewModels/PayButtonViewModel.cs b/BTCPayServer/Models/StoreViewModels/PayButtonViewModel.cs index 6c688a26b..76fa3530b 100644 --- a/BTCPayServer/Models/StoreViewModels/PayButtonViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/PayButtonViewModel.cs @@ -41,5 +41,7 @@ namespace BTCPayServer.Models.StoreViewModels public List CurrencyDropdown { get; set; } public string PayButtonImageUrl { get; set; } public string PayButtonText { get; set; } + public bool UseModal { get; set; } + public bool JsonResponse { get; set; } } } diff --git a/BTCPayServer/Views/Stores/PayButton.cshtml b/BTCPayServer/Views/Stores/PayButton.cshtml index 787dfac77..5ea5da217 100644 --- a/BTCPayServer/Views/Stores/PayButton.cshtml +++ b/BTCPayServer/Views/Stores/PayButton.cshtml @@ -47,6 +47,11 @@
    +
    + +
    KeyPermissionsLabelKeyPermissions Actions
    @keyData.Label @keyData.Id @if (string.IsNullOrEmpty(keyData.Permissions)) diff --git a/BTCPayServer/Views/Manage/AddApiKey.cshtml b/BTCPayServer/Views/Manage/AddApiKey.cshtml index e992c0a38..f5316c545 100644 --- a/BTCPayServer/Views/Manage/AddApiKey.cshtml +++ b/BTCPayServer/Views/Manage/AddApiKey.cshtml @@ -27,6 +27,14 @@
    + +
    +
    + + + +
    +
    @if (Model.IsServerAdmin) {
    diff --git a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml index 136d480a3..612d9e2f7 100644 --- a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml +++ b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml @@ -35,6 +35,14 @@
    +
    +
    + + + +
    +
    + @if (Model.PermissionsFormatted.Contains(APIKeyConstants.Permissions.ServerManagement) && (Model.IsServerAdmin || Model.Strict)) {
    From 501c3241b543c5941ec9e5d7ecc14cf1edf9661f Mon Sep 17 00:00:00 2001 From: Kukks Date: Tue, 25 Feb 2020 15:00:44 +0100 Subject: [PATCH 220/810] consolidate migrations --- .../20200119130108_ExtendApiKeys.cs | 10 + .../20200225133433_AddApiKeyLabel.Designer.cs | 849 ------------------ .../20200225133433_AddApiKeyLabel.cs | 22 - 3 files changed, 10 insertions(+), 871 deletions(-) delete mode 100644 BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.Designer.cs delete mode 100644 BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.cs diff --git a/BTCPayServer.Data/Migrations/20200119130108_ExtendApiKeys.cs b/BTCPayServer.Data/Migrations/20200119130108_ExtendApiKeys.cs index 9eeb5b83c..05fa236cf 100644 --- a/BTCPayServer.Data/Migrations/20200119130108_ExtendApiKeys.cs +++ b/BTCPayServer.Data/Migrations/20200119130108_ExtendApiKeys.cs @@ -26,6 +26,11 @@ namespace BTCPayServer.Migrations table: "ApiKeys", maxLength: 50, nullable: true); + + migrationBuilder.AddColumn( + name: "Label", + table: "ApiKeys", + nullable: true); migrationBuilder.CreateIndex( name: "IX_ApiKeys_UserId", @@ -68,6 +73,11 @@ namespace BTCPayServer.Migrations migrationBuilder.DropColumn( name: "UserId", table: "ApiKeys"); + + migrationBuilder.DropColumn( + name: "Label", + table: "ApiKeys"); + } } } diff --git a/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.Designer.cs b/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.Designer.cs deleted file mode 100644 index a73d3f0bc..000000000 --- a/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.Designer.cs +++ /dev/null @@ -1,849 +0,0 @@ -// -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("20200225133433_AddApiKeyLabel")] - partial class AddApiKeyLabel - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.1"); - - modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => - { - b.Property("Id") - .HasColumnType("TEXT") - .HasMaxLength(50); - - b.Property("Label") - .HasColumnType("TEXT"); - - b.Property("Permissions") - .HasColumnType("TEXT"); - - b.Property("StoreId") - .HasColumnType("TEXT") - .HasMaxLength(50); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("TEXT") - .HasMaxLength(50); - - b.HasKey("Id"); - - b.HasIndex("StoreId"); - - b.HasIndex("UserId"); - - b.ToTable("ApiKeys"); - }); - - modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => - { - b.Property("Address") - .HasColumnType("TEXT"); - - b.Property("CreatedTime") - .HasColumnType("TEXT"); - - b.Property("InvoiceDataId") - .HasColumnType("TEXT"); - - b.HasKey("Address"); - - b.HasIndex("InvoiceDataId"); - - b.ToTable("AddressInvoices"); - }); - - modelBuilder.Entity("BTCPayServer.Data.AppData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AppType") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Settings") - .HasColumnType("TEXT"); - - b.Property("StoreDataId") - .HasColumnType("TEXT"); - - b.Property("TagAllInvoices") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("StoreDataId"); - - b.ToTable("Apps"); - }); - - modelBuilder.Entity("BTCPayServer.Data.ApplicationUser", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Email") - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.Property("NormalizedUserName") - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RequiresEmailConfirmation") - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasName("UserNameIndex"); - - b.ToTable("AspNetUsers"); - }); - - modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => - { - b.Property("InvoiceDataId") - .HasColumnType("TEXT"); - - b.Property("Address") - .HasColumnType("TEXT"); - - b.Property("Assigned") - .HasColumnType("TEXT"); - - b.Property("CryptoCode") - .HasColumnType("TEXT"); - - b.Property("UnAssigned") - .HasColumnType("TEXT"); - - b.HasKey("InvoiceDataId", "Address"); - - b.ToTable("HistoricalAddressInvoices"); - }); - - modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Blob") - .HasColumnType("BLOB"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CustomerEmail") - .HasColumnType("TEXT"); - - b.Property("ExceptionStatus") - .HasColumnType("TEXT"); - - b.Property("ItemCode") - .HasColumnType("TEXT"); - - b.Property("OrderId") - .HasColumnType("TEXT"); - - b.Property("Status") - .HasColumnType("TEXT"); - - b.Property("StoreDataId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("StoreDataId"); - - b.ToTable("Invoices"); - }); - - modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b => - { - b.Property("InvoiceDataId") - .HasColumnType("TEXT"); - - b.Property("UniqueId") - .HasColumnType("TEXT"); - - b.Property("Message") - .HasColumnType("TEXT"); - - b.Property("Timestamp") - .HasColumnType("TEXT"); - - b.HasKey("InvoiceDataId", "UniqueId"); - - b.ToTable("InvoiceEvents"); - }); - - modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Label") - .HasColumnType("TEXT"); - - b.Property("PairingTime") - .HasColumnType("TEXT"); - - b.Property("SIN") - .HasColumnType("TEXT"); - - b.Property("StoreDataId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SIN"); - - b.HasIndex("StoreDataId"); - - b.ToTable("PairedSINData"); - }); - - modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("DateCreated") - .HasColumnType("TEXT"); - - b.Property("Expiration") - .HasColumnType("TEXT"); - - b.Property("Facade") - .HasColumnType("TEXT"); - - b.Property("Label") - .HasColumnType("TEXT"); - - b.Property("SIN") - .HasColumnType("TEXT"); - - b.Property("StoreDataId") - .HasColumnType("TEXT"); - - b.Property("TokenValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("PairingCodes"); - }); - - modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Accounted") - .HasColumnType("INTEGER"); - - b.Property("Blob") - .HasColumnType("BLOB"); - - b.Property("InvoiceDataId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("InvoiceDataId"); - - b.ToTable("Payments"); - }); - - modelBuilder.Entity("BTCPayServer.Data.PaymentRequestData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Blob") - .HasColumnType("BLOB"); - - b.Property("Created") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(new DateTimeOffset(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0))); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.Property("StoreDataId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Status"); - - b.HasIndex("StoreDataId"); - - b.ToTable("PaymentRequests"); - }); - - modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("PendingInvoices"); - }); - - modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Blob") - .HasColumnType("BLOB"); - - b.Property("InvoiceDataId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("InvoiceDataId"); - - b.ToTable("RefundAddresses"); - }); - - modelBuilder.Entity("BTCPayServer.Data.SettingData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Settings"); - }); - - modelBuilder.Entity("BTCPayServer.Data.StoreData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("DefaultCrypto") - .HasColumnType("TEXT"); - - b.Property("DerivationStrategies") - .HasColumnType("TEXT"); - - b.Property("DerivationStrategy") - .HasColumnType("TEXT"); - - b.Property("SpeedPolicy") - .HasColumnType("INTEGER"); - - b.Property("StoreBlob") - .HasColumnType("BLOB"); - - b.Property("StoreCertificate") - .HasColumnType("BLOB"); - - b.Property("StoreName") - .HasColumnType("TEXT"); - - b.Property("StoreWebsite") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Stores"); - }); - - modelBuilder.Entity("BTCPayServer.Data.StoredFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("ApplicationUserId") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("StorageFileName") - .HasColumnType("TEXT"); - - b.Property("Timestamp") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationUserId"); - - b.ToTable("Files"); - }); - - modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ApplicationUserId") - .HasColumnType("TEXT"); - - b.Property("AttestationCert") - .IsRequired() - .HasColumnType("BLOB"); - - b.Property("Counter") - .HasColumnType("INTEGER"); - - b.Property("KeyHandle") - .IsRequired() - .HasColumnType("BLOB"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("PublicKey") - .IsRequired() - .HasColumnType("BLOB"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationUserId"); - - b.ToTable("U2FDevices"); - }); - - modelBuilder.Entity("BTCPayServer.Data.UserStore", b => - { - b.Property("ApplicationUserId") - .HasColumnType("TEXT"); - - b.Property("StoreDataId") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("TEXT"); - - b.HasKey("ApplicationUserId", "StoreDataId"); - - b.HasIndex("StoreDataId"); - - b.ToTable("UserStore"); - }); - - modelBuilder.Entity("BTCPayServer.Data.WalletData", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Blob") - .HasColumnType("BLOB"); - - b.HasKey("Id"); - - b.ToTable("Wallets"); - }); - - modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b => - { - b.Property("WalletDataId") - .HasColumnType("TEXT"); - - b.Property("TransactionId") - .HasColumnType("TEXT"); - - b.Property("Blob") - .HasColumnType("BLOB"); - - b.Property("Labels") - .HasColumnType("TEXT"); - - b.HasKey("WalletDataId", "TransactionId"); - - b.ToTable("WalletTransactions"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.Property("NormalizedName") - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasName("RoleNameIndex"); - - b.ToTable("AspNetRoles"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens"); - }); - - modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => - { - b.HasOne("BTCPayServer.Data.StoreData", "StoreData") - .WithMany("APIKeys") - .HasForeignKey("StoreId") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("BTCPayServer.Data.ApplicationUser", "User") - .WithMany("APIKeys") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => - { - b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") - .WithMany("AddressInvoices") - .HasForeignKey("InvoiceDataId") - .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) - .IsRequired(); - }); - - 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) - .IsRequired(); - }); - - 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.PaymentRequestData", b => - { - b.HasOne("BTCPayServer.Data.StoreData", "StoreData") - .WithMany("PaymentRequests") - .HasForeignKey("StoreDataId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => - { - b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") - .WithMany("PendingInvoices") - .HasForeignKey("Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => - { - b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") - .WithMany("RefundAddresses") - .HasForeignKey("InvoiceDataId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("BTCPayServer.Data.StoredFile", b => - { - b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") - .WithMany("StoredFiles") - .HasForeignKey("ApplicationUserId"); - }); - - modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b => - { - b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") - .WithMany("U2FDevices") - .HasForeignKey("ApplicationUserId"); - }); - - modelBuilder.Entity("BTCPayServer.Data.UserStore", b => - { - b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser") - .WithMany("UserStores") - .HasForeignKey("ApplicationUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("BTCPayServer.Data.StoreData", "StoreData") - .WithMany("UserStores") - .HasForeignKey("StoreDataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b => - { - b.HasOne("BTCPayServer.Data.WalletData", "WalletData") - .WithMany("WalletTransactions") - .HasForeignKey("WalletDataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("BTCPayServer.Data.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("BTCPayServer.Data.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("BTCPayServer.Data.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("BTCPayServer.Data.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.cs b/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.cs deleted file mode 100644 index 6db60f5c0..000000000 --- a/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace BTCPayServer.Migrations -{ - public partial class AddApiKeyLabel : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Label", - table: "ApiKeys", - nullable: true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Label", - table: "ApiKeys"); - } - } -} From ec80787120fc5f82e92ceb5b886cd45d1d40e9f5 Mon Sep 17 00:00:00 2001 From: Kukks Date: Tue, 25 Feb 2020 15:33:04 +0100 Subject: [PATCH 221/810] fix --- BTCPayServer/Controllers/StoresController.BTCLike.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/Controllers/StoresController.BTCLike.cs b/BTCPayServer/Controllers/StoresController.BTCLike.cs index d1da40d96..e7e2bc966 100644 --- a/BTCPayServer/Controllers/StoresController.BTCLike.cs +++ b/BTCPayServer/Controllers/StoresController.BTCLike.cs @@ -142,7 +142,7 @@ namespace BTCPayServer.Controllers [HttpPost] [Route("{storeId}/derivations/{cryptoCode}")] - public async Task AddDerivationScheme(string storeId, [FromBody] DerivationSchemeViewModel vm, + public async Task AddDerivationScheme(string storeId, [FromForm] DerivationSchemeViewModel vm, string cryptoCode) { vm.CryptoCode = cryptoCode; From b71fd1653e57361bfad56650c1f49f18e4b1c4de Mon Sep 17 00:00:00 2001 From: Kukks Date: Tue, 25 Feb 2020 16:44:19 +0100 Subject: [PATCH 222/810] remove changelly fiat option closes #728 --- .../Controllers/StoresController.Changelly.cs | 5 +-- .../UpdateChangellySettingsViewModel.cs | 3 -- BTCPayServer/Payments/Changelly/Changelly.cs | 32 +++---------------- .../Changelly/ChangellyClientProvider.cs | 5 +-- .../Payments/Changelly/ChangellySettings.cs | 1 - .../Stores/UpdateChangellySettings.cshtml | 5 --- 6 files changed, 9 insertions(+), 42 deletions(-) diff --git a/BTCPayServer/Controllers/StoresController.Changelly.cs b/BTCPayServer/Controllers/StoresController.Changelly.cs index 68b475e33..ad0bb4399 100644 --- a/BTCPayServer/Controllers/StoresController.Changelly.cs +++ b/BTCPayServer/Controllers/StoresController.Changelly.cs @@ -33,8 +33,6 @@ namespace BTCPayServer.Controllers vm.ChangellyMerchantId = existing.ChangellyMerchantId; vm.Enabled = existing.Enabled; vm.AmountMarkupPercentage = existing.AmountMarkupPercentage; - vm.ShowFiat = existing.ShowFiat; - } [HttpPost] @@ -60,8 +58,7 @@ namespace BTCPayServer.Controllers ApiUrl = vm.ApiUrl, ChangellyMerchantId = vm.ChangellyMerchantId, Enabled = vm.Enabled, - AmountMarkupPercentage = vm.AmountMarkupPercentage, - ShowFiat = vm.ShowFiat + AmountMarkupPercentage = vm.AmountMarkupPercentage }; switch (command) diff --git a/BTCPayServer/Models/StoreViewModels/UpdateChangellySettingsViewModel.cs b/BTCPayServer/Models/StoreViewModels/UpdateChangellySettingsViewModel.cs index e143e7453..9a3db8815 100644 --- a/BTCPayServer/Models/StoreViewModels/UpdateChangellySettingsViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/UpdateChangellySettingsViewModel.cs @@ -17,9 +17,6 @@ namespace BTCPayServer.Models.StoreViewModels [Display(Name = "Optional, Changelly Merchant Id")] public string ChangellyMerchantId { get; set; } - [Display(Name = "Show Fiat Currencies as option in conversion")] - public bool ShowFiat { get; set; } = true; - [Required] [Range(0, 100)] [Display(Name = diff --git a/BTCPayServer/Payments/Changelly/Changelly.cs b/BTCPayServer/Payments/Changelly/Changelly.cs index ca5e921b6..080e03f81 100644 --- a/BTCPayServer/Payments/Changelly/Changelly.cs +++ b/BTCPayServer/Payments/Changelly/Changelly.cs @@ -16,13 +16,11 @@ namespace BTCPayServer.Payments.Changelly public class Changelly { private readonly string _apisecret; - private readonly bool _showFiat; private readonly HttpClient _httpClient; - public Changelly(HttpClient httpClient, string apiKey, string apiSecret, string apiUrl, bool showFiat = true) + public Changelly(HttpClient httpClient, string apiKey, string apiSecret, string apiUrl) { _apisecret = apiSecret; - _showFiat = showFiat; _httpClient = httpClient; _httpClient.BaseAddress = new Uri(apiUrl); _httpClient.DefaultRequestHeaders.Add("api-key", apiKey); @@ -62,36 +60,16 @@ namespace BTCPayServer.Payments.Changelly public virtual async Task> GetCurrenciesFull() { - const string message = @"{ + const string message = @"{ ""jsonrpc"": ""2.0"", ""id"": 1, ""method"": ""getCurrenciesFull"", ""params"": [] }"; - var result = await PostToApi>(message); - var appendedResult = _showFiat - ? result.Result.Concat(new[] - { - new CurrencyFull() - { - Enable = true, - Name = "EUR", - FullName = "Euro", - PayInConfirmations = 0, - ImageLink = "https://changelly.com/api/coins/eur.png" - }, - new CurrencyFull() - { - Enable = true, - Name = "USD", - FullName = "US Dollar", - PayInConfirmations = 0, - ImageLink = "https://changelly.com/api/coins/usd.png" - } - }) - : result.Result; - return appendedResult; + var result = await PostToApi>(message); + + return result.Result; } public virtual async Task GetExchangeAmount(string fromCurrency, diff --git a/BTCPayServer/Payments/Changelly/ChangellyClientProvider.cs b/BTCPayServer/Payments/Changelly/ChangellyClientProvider.cs index cb490eae4..0428f9804 100644 --- a/BTCPayServer/Payments/Changelly/ChangellyClientProvider.cs +++ b/BTCPayServer/Payments/Changelly/ChangellyClientProvider.cs @@ -61,8 +61,9 @@ namespace BTCPayServer.Payments.Changelly throw new ChangellyException("Changelly not enabled for this store"); } - var changelly = new Changelly(_httpClientFactory.CreateClient("Changelly"), changellySettings.ApiKey, changellySettings.ApiSecret, - changellySettings.ApiUrl, changellySettings.ShowFiat); + var changelly = new Changelly(_httpClientFactory.CreateClient("Changelly"), changellySettings.ApiKey, + changellySettings.ApiSecret, + changellySettings.ApiUrl); _clientCache.AddOrReplace(storeId, changelly); return changelly; } diff --git a/BTCPayServer/Payments/Changelly/ChangellySettings.cs b/BTCPayServer/Payments/Changelly/ChangellySettings.cs index 56d6b7567..784852a49 100644 --- a/BTCPayServer/Payments/Changelly/ChangellySettings.cs +++ b/BTCPayServer/Payments/Changelly/ChangellySettings.cs @@ -8,7 +8,6 @@ namespace BTCPayServer.Payments.Changelly public bool Enabled { get; set; } public string ChangellyMerchantId { get; set; } public decimal AmountMarkupPercentage { get; set; } - public bool ShowFiat { get; set; } public bool IsConfigured() { diff --git a/BTCPayServer/Views/Stores/UpdateChangellySettings.cshtml b/BTCPayServer/Views/Stores/UpdateChangellySettings.cshtml index 524ab3846..c8bfff9cc 100644 --- a/BTCPayServer/Views/Stores/UpdateChangellySettings.cshtml +++ b/BTCPayServer/Views/Stores/UpdateChangellySettings.cshtml @@ -38,11 +38,6 @@
    -
    - - - -
    From 5a93857b4acc3930a33049c5156cef90afdd72e7 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Tue, 25 Feb 2020 16:08:57 -0600 Subject: [PATCH 223/810] Simplifying delegate invoke Ref: https://github.com/btcpayserver/btcpayserver/commit/00747906849f093712c3907c99404c55b3defa66#r37477529 --- .../Providers/HttpClientRequestMaker.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs b/BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs index 88a5e281e..7c566d087 100644 --- a/BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs +++ b/BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs @@ -196,14 +196,8 @@ namespace BTCPayServer.Services.Rates throw new APIException(text); } api.ProcessResponse(new InternalHttpWebResponse(webHttpResponse)); - // local reference to handle delegate becoming null, extended discussion here: - // https://github.com/btcpayserver/btcpayserver/commit/00747906849f093712c3907c99404c55b3defa66#r37022103 - var requestStateChanged = RequestStateChanged; - if (requestStateChanged != null) - { - requestStateChanged(this, RequestMakerState.Finished, text); - return text; - } + + RequestStateChanged?.Invoke(this, RequestMakerState.Finished, text); return text; } catch (Exception arg) From 78f73132ed5ce8b6b41c7bb7e595fa40406fee9b Mon Sep 17 00:00:00 2001 From: Pavlenex Date: Wed, 26 Feb 2020 06:00:46 +0100 Subject: [PATCH 224/810] Delete docs folder (#1354) --- Docs/Themes.md | 51 -------------------------------------------------- 1 file changed, 51 deletions(-) delete mode 100644 Docs/Themes.md diff --git a/Docs/Themes.md b/Docs/Themes.md deleted file mode 100644 index 7f61e8a77..000000000 --- a/Docs/Themes.md +++ /dev/null @@ -1,51 +0,0 @@ -# Developing and extending themes - -The BTCPay Server user interface is built on a customized version of Bootstrap that supports [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/--*). -This allows us to change theme related settings like fonts and colors without affecting the [`bootstrap.css`](#Notes-on-bootstrapcss). -Also we can provide just the relevant customized parts instead of shipping a whole `bootstrap.css` file for each theme. - -Take a look at the [predefined themes](../BTCPayServer/wwwroot/main/themes) to get an overview of this approach. - -## Modifying existing themes - -The custom property definitions in the `:root` selector are divided into several sections, that can be seen as a cascade: - -- The first section contains general definitions (i.e. for custom brand and neutral colors). -- The second section defines variables for specific purposes. - Here you can map the general definitions or create additional ones. -- The third section contains definitions for specific parts of the page, sections or components. - Here you should try to reuse definitions from above as much as possible to provide a consistent look and feel. - -The variables defined in a theme file get used in the [`site.css`](../BTCPayServer/wwwroot/main/site.css) and [`creative.css`](../BTCPayServer/wwwroot/main/bootstrap4-creativestart/creative.css) files. - -### Overriding Bootstrap selectors - -In addition to the variables you can also provide styles by directly adding CSS selectors to this file. -This can be seen as a last resort in case there is no variable for something you want to change or some minor tweaking. - -### Adding theme variables - -In general it is a good idea to introduce specific variables for special purposes (like setting the link colors of a specific section). -This allows us to address individual portions of the styles without affecting other parts which might be tight to a general variable. - -For cases in which you want to introduce new variables that are used across all themes, add them to the `site.css` file. -This file contains our modifications of the Bootstrap styles. -Refrain from modifying `bootstrap.css` directly – see the [additional notes](#Notes-on-bootstrapcss) for the reasoning behind this. - -## Adding a new theme - -You should copy one of our predefined themes and change the variables to fit your needs. - -To test and play around with the adjustments, you can also use the developer tools of the browser: -Inspect the `` element and modify the variables in the `:root` section of the styles inspector. - -## Notes on bootstrap.css - -The `bootstrap.css` file itself is generated based on what the original vendor `bootstrap.css` provides. - -Right now [Bootstrap](https://getbootstrap.com/docs/4.3/getting-started/theming/) does not use custom properties, but in the future it is likely that they might switch to this approach as well. -Until then we created a build script [in this repo](https://github.com/dennisreimann/btcpayserver-ui-prototype) which generates the `bootstrap.css` file we are using here. - -The general approach should be to not modify the `bootstrap.css`, so that we can keep it easily updatable. -The initial modifications of this file were made in order to allow for this themeing approach. -Because bootstrap has colors spread all over the place we'd otherwise have to override mostly everything, that's why these general modifications are in the main `bootstrap.css` file. From 0c7f35b0003678caee98fe179c2cbe9339bce649 Mon Sep 17 00:00:00 2001 From: Kukks Date: Wed, 26 Feb 2020 09:10:27 +0100 Subject: [PATCH 225/810] fix swagger gen --- BTCPayServer/Controllers/StoresController.BTCLike.cs | 3 ++- BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/BTCPayServer/Controllers/StoresController.BTCLike.cs b/BTCPayServer/Controllers/StoresController.BTCLike.cs index e7e2bc966..1926cecb1 100644 --- a/BTCPayServer/Controllers/StoresController.BTCLike.cs +++ b/BTCPayServer/Controllers/StoresController.BTCLike.cs @@ -141,7 +141,8 @@ namespace BTCPayServer.Controllers } [HttpPost] - [Route("{storeId}/derivations/{cryptoCode}")] + [Route("{storeId}/derivations/{cryptoCode}")] + [ApiExplorerSettings(IgnoreApi = true)] public async Task AddDerivationScheme(string storeId, [FromForm] DerivationSchemeViewModel vm, string cryptoCode) { diff --git a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml index 612d9e2f7..303e171aa 100644 --- a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml +++ b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml @@ -42,7 +42,12 @@
    - + @if (!Model.PermissionsFormatted.Any()) + { +
    +

    There are no associated permissions to the API key being requested here. The application cannot do anything with your BTCPay account other than validating your account exists.

    +
    + } @if (Model.PermissionsFormatted.Contains(APIKeyConstants.Permissions.ServerManagement) && (Model.IsServerAdmin || Model.Strict)) {
    From d9ea9fbffd8d1817297b0a9d2a8a99b56156cf97 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Wed, 26 Feb 2020 17:34:32 +0900 Subject: [PATCH 226/810] Fix colspan --- BTCPayServer/Views/Manage/APIKeys.cshtml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BTCPayServer/Views/Manage/APIKeys.cshtml b/BTCPayServer/Views/Manage/APIKeys.cshtml index 5c6656ef5..4e4a69cca 100644 --- a/BTCPayServer/Views/Manage/APIKeys.cshtml +++ b/BTCPayServer/Views/Manage/APIKeys.cshtml @@ -38,13 +38,13 @@ @if (!Model.ApiKeyDatas.Any()) {
    + No API keys
    + Generate new key
    - Remove + Remove
    + @@ -22,6 +23,9 @@ { + From 44d467398161c053bf3b9c19f9c142169990868b Mon Sep 17 00:00:00 2001 From: rockstardev Date: Fri, 28 Feb 2020 16:58:09 -0600 Subject: [PATCH 234/810] Removing reference to InvoicePaymentsPartial.cshtml from Invoice.cshtml --- BTCPayServer/Views/Invoice/Invoice.cshtml | 43 ++++++++++++++++++- .../Invoice/InvoicePaymentsPartial.cshtml | 3 +- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/BTCPayServer/Views/Invoice/Invoice.cshtml b/BTCPayServer/Views/Invoice/Invoice.cshtml index d2e5d39c5..0dc1ed26c 100644 --- a/BTCPayServer/Views/Invoice/Invoice.cshtml +++ b/BTCPayServer/Views/Invoice/Invoice.cshtml @@ -183,7 +183,48 @@ } - +
    +
    +

    Paid summary

    +
    Payment methodAddress Rate Paid Due
    @payment.PaymentMethod + @payment.Address + @payment.Rate @payment.Paid @payment.Due
    + + + + + + + @if (Model.StatusException == InvoiceExceptionStatus.PaidOver) + { + + } + + + + @foreach (var payment in Model.CryptoPayments) + { + + + + + + @if (Model.StatusException == InvoiceExceptionStatus.PaidOver) + { + + } + + } + +
    Payment methodRatePaidDueOverpaid
    @payment.PaymentMethod@payment.Rate@payment.Paid@payment.Due@payment.Overpaid
    +
    +
    + @{ + var grouped = Model.Payments.GroupBy(payment => payment.GetPaymentMethodId().PaymentType); + } + @foreach (var paymentGroup in grouped) + { + + } +
    diff --git a/BTCPayServer/Views/Invoice/InvoicePaymentsPartial.cshtml b/BTCPayServer/Views/Invoice/InvoicePaymentsPartial.cshtml index 3d9d0806a..73b9e997b 100644 --- a/BTCPayServer/Views/Invoice/InvoicePaymentsPartial.cshtml +++ b/BTCPayServer/Views/Invoice/InvoicePaymentsPartial.cshtml @@ -40,8 +40,7 @@
    @{ - var grouped = Model.Payments.GroupBy(payment => payment.GetPaymentMethodId().PaymentType); - + var grouped = Model.Payments.GroupBy(payment => payment.GetPaymentMethodId().PaymentType); } @foreach (var paymentGroup in grouped) { From 2934c27ee55bc3b1f6319bde21e5a26a4dc35adb Mon Sep 17 00:00:00 2001 From: rockstardev Date: Fri, 28 Feb 2020 17:04:56 -0600 Subject: [PATCH 235/810] Commenting decision to leave partial --- BTCPayServer/Views/Invoice/ListInvoices.cshtml | 3 ++- ...ymentsPartial.cshtml => ListInvoicesPaymentsPartial.cshtml} | 0 2 files changed, 2 insertions(+), 1 deletion(-) rename BTCPayServer/Views/Invoice/{InvoicePaymentsPartial.cshtml => ListInvoicesPaymentsPartial.cshtml} (100%) diff --git a/BTCPayServer/Views/Invoice/ListInvoices.cshtml b/BTCPayServer/Views/Invoice/ListInvoices.cshtml index 41f5ecd34..c7e821fb3 100644 --- a/BTCPayServer/Views/Invoice/ListInvoices.cshtml +++ b/BTCPayServer/Views/Invoice/ListInvoices.cshtml @@ -274,7 +274,8 @@
    - + @* Leaving this as partial because it abstracts complexity of Invoice Payments *@ +
    diff --git a/BTCPayServer/Views/Invoice/InvoicePaymentsPartial.cshtml b/BTCPayServer/Views/Invoice/ListInvoicesPaymentsPartial.cshtml similarity index 100% rename from BTCPayServer/Views/Invoice/InvoicePaymentsPartial.cshtml rename to BTCPayServer/Views/Invoice/ListInvoicesPaymentsPartial.cshtml From a303e793b4d01616b35419d1612cefae2fa6c527 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Fri, 28 Feb 2020 23:15:06 -0600 Subject: [PATCH 236/810] Fixing CanCreateApiKeys test admin user check --- BTCPayServer.Tests/ServerTester.cs | 3 +++ BTCPayServer.Tests/TestAccount.cs | 1 + BTCPayServer/Controllers/AccountController.cs | 13 +++++-------- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/BTCPayServer.Tests/ServerTester.cs b/BTCPayServer.Tests/ServerTester.cs index b83bd3159..a5321e1ed 100644 --- a/BTCPayServer.Tests/ServerTester.cs +++ b/BTCPayServer.Tests/ServerTester.cs @@ -53,6 +53,9 @@ namespace BTCPayServer.Tests { NBXplorerUri = ExplorerClient.Address, TestDatabase = Enum.Parse(GetEnvironment("TESTS_DB", TestDatabases.Postgres.ToString()), true), + // TODO: The fact that we use same conn string as development database can cause huge problems with tests + // since in dev we already can have some users / stores registered, while on CI database is being initalized + // for the first time and first registered user gets admin status by default Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"), MySQL = GetEnvironment("TESTS_MYSQL", "User ID=root;Host=127.0.0.1;Port=33036;Database=btcpayserver") }; diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index d57eb54df..4c057e471 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -146,6 +146,7 @@ namespace BTCPayServer.Tests }; await account.Register(RegisterDetails); UserId = account.RegisteredUserId; + IsAdmin = account.RegisteredAdmin; } public RegisterViewModel RegisterDetails{ get; set; } diff --git a/BTCPayServer/Controllers/AccountController.cs b/BTCPayServer/Controllers/AccountController.cs index fde8d4e9d..2ca29ef6f 100644 --- a/BTCPayServer/Controllers/AccountController.cs +++ b/BTCPayServer/Controllers/AccountController.cs @@ -447,12 +447,13 @@ namespace BTCPayServer.Controllers var settings = await _SettingsRepository.GetSettingAsync(); settings.FirstRun = false; await _SettingsRepository.UpdateSetting(settings); - if(_Options.DisableRegistration) + if (_Options.DisableRegistration) { // Once the admin user has been created lock subsequent user registrations (needs to be disabled for unit tests that require multiple users). policies.LockSubscription = true; await _SettingsRepository.UpdateSetting(policies); } + RegisteredAdmin = true; } var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); @@ -479,13 +480,9 @@ namespace BTCPayServer.Controllers return View(model); } - /// - /// Test property - /// - public string RegisteredUserId - { - get; set; - } + // Properties used by tests + public string RegisteredUserId { get; set; } + public bool RegisteredAdmin { get; set; } [HttpGet] public async Task Logout() From 56380a5fb3afb935b09b324c58dc1ff2c7bfc543 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Fri, 28 Feb 2020 23:15:14 -0600 Subject: [PATCH 237/810] Formatting code --- BTCPayServer/Controllers/AccountController.cs | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/BTCPayServer/Controllers/AccountController.cs b/BTCPayServer/Controllers/AccountController.cs index 2ca29ef6f..acaac444f 100644 --- a/BTCPayServer/Controllers/AccountController.cs +++ b/BTCPayServer/Controllers/AccountController.cs @@ -39,7 +39,7 @@ namespace BTCPayServer.Controllers SettingsRepository _SettingsRepository; Configuration.BTCPayServerOptions _Options; private readonly BTCPayServerEnvironment _btcPayServerEnvironment; - public U2FService _u2FService; + public U2FService _u2FService; ILogger _logger; public AccountController( @@ -75,7 +75,7 @@ namespace BTCPayServer.Controllers [AllowAnonymous] public async Task Login(string returnUrl = null) { - + if (User.Identity.IsAuthenticated && string.IsNullOrEmpty(returnUrl)) return RedirectToLocal(); // Clear the existing external cookie to ensure a clean login process @@ -85,7 +85,7 @@ namespace BTCPayServer.Controllers { SetInsecureFlags(); } - + ViewData["ReturnUrl"] = returnUrl; return View(); } @@ -126,7 +126,7 @@ namespace BTCPayServer.Controllers if (await _userManager.CheckPasswordAsync(user, model.Password)) { LoginWith2faViewModel twoFModel = null; - + if (user.TwoFactorEnabled) { // we need to do an actual sign in attempt so that 2fa can function in next step @@ -145,14 +145,14 @@ namespace BTCPayServer.Controllers } else { - var incrementAccessFailedResult = await _userManager.AccessFailedAsync(user); - ModelState.AddModelError(string.Empty, "Invalid login attempt."); - return View(model); - + var incrementAccessFailedResult = await _userManager.AccessFailedAsync(user); + ModelState.AddModelError(string.Empty, "Invalid login attempt."); + return View(model); + } } - - + + var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true); if (result.Succeeded) { @@ -215,7 +215,7 @@ namespace BTCPayServer.Controllers { return RedirectToAction("Login"); } - + ViewData["ReturnUrl"] = returnUrl; var user = await _userManager.FindByIdAsync(viewModel.UserId); @@ -276,7 +276,7 @@ namespace BTCPayServer.Controllers return View("SecondaryLogin", new SecondaryLoginViewModel() { LoginWith2FaViewModel = new LoginWith2faViewModel { RememberMe = rememberMe }, - LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id))? await BuildU2FViewModel(rememberMe, user): null + LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id)) ? await BuildU2FViewModel(rememberMe, user) : null }); } @@ -322,7 +322,7 @@ namespace BTCPayServer.Controllers return View("SecondaryLogin", new SecondaryLoginViewModel() { LoginWith2FaViewModel = model, - LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id))? await BuildU2FViewModel(rememberMe, user): null + LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id)) ? await BuildU2FViewModel(rememberMe, user) : null }); } } @@ -463,7 +463,7 @@ namespace BTCPayServer.Controllers _EmailSenderFactory.GetEmailSender().SendEmailConfirmation(model.Email, callbackUrl); if (!policies.RequiresConfirmedEmail) { - if(logon) + if (logon) await _signInManager.SignInAsync(user, isPersistent: false); return RedirectToLocal(returnUrl); } @@ -536,7 +536,7 @@ namespace BTCPayServer.Controllers var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme); _EmailSenderFactory.GetEmailSender().SendEmail(model.Email, "Reset Password", $"Please reset your password by clicking here: link"); - + return RedirectToAction(nameof(ForgotPasswordConfirmation)); } @@ -622,8 +622,8 @@ namespace BTCPayServer.Controllers return RedirectToAction(nameof(HomeController.Index), "Home"); } } - - + + private bool CanLoginOrRegister() { return _btcPayServerEnvironment.IsDevelopping || _btcPayServerEnvironment.IsSecure; @@ -636,7 +636,7 @@ namespace BTCPayServer.Controllers Severity = StatusMessageModel.StatusSeverity.Error, Message = "You cannot login over an insecure connection. Please use HTTPS or Tor." }); - + ViewData["disabled"] = true; } From 27f964e2a19ed5aff70e3eee489a528cc7a2c89e Mon Sep 17 00:00:00 2001 From: pavlenex Date: Sat, 29 Feb 2020 10:46:07 +0100 Subject: [PATCH 238/810] add enviroment configuration --- .github/ISSUE_TEMPLATE/bug_report.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index fcd47f7d3..4f99415e6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -14,6 +14,11 @@ A clear and concise description of what the bug is. **Logs (if applicable)** Basic logs can be found in Server Settings > Logs. +**Profile Configuration** +If you're reporting a deployment issue, paste your configuration: + +`cat /etc/profile.d/btcpay-env.sh` + **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' From efda8ff5bdff91d684b6304e6ca7b2850dce2e6f Mon Sep 17 00:00:00 2001 From: Umar Bolatov Date: Sun, 1 Mar 2020 14:16:24 -0800 Subject: [PATCH 239/810] Specify QR code error correction level explicitly fix #1332 --- BTCPayServer/Views/Home/BitpayTranslator.cshtml | 3 ++- BTCPayServer/Views/Manage/EnableAuthenticator.cshtml | 3 ++- BTCPayServer/Views/Server/CLightningRestServices.cshtml | 3 ++- BTCPayServer/Views/Server/LightningWalletServices.cshtml | 3 ++- BTCPayServer/Views/Server/LndServices.cshtml | 3 ++- BTCPayServer/Views/Server/P2PService.cshtml | 3 ++- BTCPayServer/Views/Server/RPCService.cshtml | 3 ++- 7 files changed, 14 insertions(+), 7 deletions(-) diff --git a/BTCPayServer/Views/Home/BitpayTranslator.cshtml b/BTCPayServer/Views/Home/BitpayTranslator.cshtml index 302563c85..3c2154301 100644 --- a/BTCPayServer/Views/Home/BitpayTranslator.cshtml +++ b/BTCPayServer/Views/Home/BitpayTranslator.cshtml @@ -55,7 +55,8 @@ text: @Safe.Json(Model.BitcoinUri), width: 200, height: 200, - useSVG: true + useSVG: true, + correctLevel : QRCode.CorrectLevel.M }); $("#qrCode > img").css({ "margin": "auto" }); diff --git a/BTCPayServer/Views/Manage/EnableAuthenticator.cshtml b/BTCPayServer/Views/Manage/EnableAuthenticator.cshtml index 1785183e6..a0b4d6c49 100644 --- a/BTCPayServer/Views/Manage/EnableAuthenticator.cshtml +++ b/BTCPayServer/Views/Manage/EnableAuthenticator.cshtml @@ -56,7 +56,8 @@ text: @Safe.Json(Model.AuthenticatorUri), width: 200, height: 200, - useSVG: true + useSVG: true, + correctLevel : QRCode.CorrectLevel.M }); $("#qrCode > img").css({ "margin": "auto" }); diff --git a/BTCPayServer/Views/Server/CLightningRestServices.cshtml b/BTCPayServer/Views/Server/CLightningRestServices.cshtml index 2aab98834..4c2b66dd6 100644 --- a/BTCPayServer/Views/Server/CLightningRestServices.cshtml +++ b/BTCPayServer/Views/Server/CLightningRestServices.cshtml @@ -158,7 +158,8 @@ text: @Safe.Json(Model.QRCode), width: 200, height: 200, - useSVG: true + useSVG: true, + correctLevel : QRCode.CorrectLevel.M }); } diff --git a/BTCPayServer/Views/Server/LightningWalletServices.cshtml b/BTCPayServer/Views/Server/LightningWalletServices.cshtml index a1ab56705..156068b8f 100644 --- a/BTCPayServer/Views/Server/LightningWalletServices.cshtml +++ b/BTCPayServer/Views/Server/LightningWalletServices.cshtml @@ -73,7 +73,8 @@ text: @Safe.Json(Model.ServiceLink), width: 200, height: 200, - useSVG: true + useSVG: true, + correctLevel : QRCode.CorrectLevel.M }); } diff --git a/BTCPayServer/Views/Server/LndServices.cshtml b/BTCPayServer/Views/Server/LndServices.cshtml index 1dead47e2..d50f49d0d 100644 --- a/BTCPayServer/Views/Server/LndServices.cshtml +++ b/BTCPayServer/Views/Server/LndServices.cshtml @@ -187,7 +187,8 @@ text: @Safe.Json(Model.QRCode), width: 200, height: 200, - useSVG: true + useSVG: true, + correctLevel : QRCode.CorrectLevel.M }); } diff --git a/BTCPayServer/Views/Server/P2PService.cshtml b/BTCPayServer/Views/Server/P2PService.cshtml index cad8fb1de..22501e34e 100644 --- a/BTCPayServer/Views/Server/P2PService.cshtml +++ b/BTCPayServer/Views/Server/P2PService.cshtml @@ -101,7 +101,8 @@ text: @Safe.Json(Model.ServiceLink), width: 200, height: 200, - useSVG: true + useSVG: true, + correctLevel : QRCode.CorrectLevel.M }); } diff --git a/BTCPayServer/Views/Server/RPCService.cshtml b/BTCPayServer/Views/Server/RPCService.cshtml index 5e576f01b..6efe9657a 100644 --- a/BTCPayServer/Views/Server/RPCService.cshtml +++ b/BTCPayServer/Views/Server/RPCService.cshtml @@ -98,7 +98,8 @@ text: @Safe.Json(Model.ServiceLink), width: 200, height: 200, - useSVG: true + useSVG: true, + correctLevel : QRCode.CorrectLevel.M }); } From f8520201cec65fbd9a206caa2526e75da7ff8df7 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sun, 1 Mar 2020 19:46:05 -0600 Subject: [PATCH 240/810] Using tuple for payments partial model --- BTCPayServer/Views/Invoice/Invoice.cshtml | 42 +------------------ .../Views/Invoice/ListInvoices.cshtml | 2 +- .../ListInvoicesPaymentsPartial.cshtml | 17 +++++--- 3 files changed, 14 insertions(+), 47 deletions(-) diff --git a/BTCPayServer/Views/Invoice/Invoice.cshtml b/BTCPayServer/Views/Invoice/Invoice.cshtml index 0dc1ed26c..9c796fad2 100644 --- a/BTCPayServer/Views/Invoice/Invoice.cshtml +++ b/BTCPayServer/Views/Invoice/Invoice.cshtml @@ -183,47 +183,7 @@
    } -
    -
    -

    Paid summary

    - - - - - - - - @if (Model.StatusException == InvoiceExceptionStatus.PaidOver) - { - - } - - - - @foreach (var payment in Model.CryptoPayments) - { - - - - - - @if (Model.StatusException == InvoiceExceptionStatus.PaidOver) - { - - } - - } - -
    Payment methodRatePaidDueOverpaid
    @payment.PaymentMethod@payment.Rate@payment.Paid@payment.Due@payment.Overpaid
    -
    -
    - @{ - var grouped = Model.Payments.GroupBy(payment => payment.GetPaymentMethodId().PaymentType); - } - @foreach (var paymentGroup in grouped) - { - - } +
    diff --git a/BTCPayServer/Views/Invoice/ListInvoices.cshtml b/BTCPayServer/Views/Invoice/ListInvoices.cshtml index c7e821fb3..1ef624824 100644 --- a/BTCPayServer/Views/Invoice/ListInvoices.cshtml +++ b/BTCPayServer/Views/Invoice/ListInvoices.cshtml @@ -275,7 +275,7 @@
    @* Leaving this as partial because it abstracts complexity of Invoice Payments *@ - +
    diff --git a/BTCPayServer/Views/Invoice/ListInvoicesPaymentsPartial.cshtml b/BTCPayServer/Views/Invoice/ListInvoicesPaymentsPartial.cshtml index 73b9e997b..ad9cd4ea4 100644 --- a/BTCPayServer/Views/Invoice/ListInvoicesPaymentsPartial.cshtml +++ b/BTCPayServer/Views/Invoice/ListInvoicesPaymentsPartial.cshtml @@ -1,4 +1,5 @@ -@model InvoiceDetailsModel +@model (InvoiceDetailsModel Invoice, bool ShowAddress) +@{ var invoice = Model.Invoice; } @inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
    @@ -8,28 +9,34 @@ Payment method + @if (Model.ShowAddress) + { Address + } Rate Paid Due - @if (Model.StatusException == InvoiceExceptionStatus.PaidOver) + @if (invoice.StatusException == InvoiceExceptionStatus.PaidOver) { Overpaid } - @foreach (var payment in Model.CryptoPayments) + @foreach (var payment in invoice.CryptoPayments) { @payment.PaymentMethod + @if (Model.ShowAddress) + { @payment.Address + } @payment.Rate @payment.Paid @payment.Due - @if (Model.StatusException == InvoiceExceptionStatus.PaidOver) + @if (invoice.StatusException == InvoiceExceptionStatus.PaidOver) { @payment.Overpaid } @@ -40,7 +47,7 @@
    @{ - var grouped = Model.Payments.GroupBy(payment => payment.GetPaymentMethodId().PaymentType); + var grouped = invoice.Payments.GroupBy(payment => payment.GetPaymentMethodId().PaymentType); } @foreach (var paymentGroup in grouped) { From 7f6d27cc5b563d7b25e9407144d63842cc25f16e Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sun, 1 Mar 2020 20:51:25 -0600 Subject: [PATCH 241/810] Fixing modal Open Wallet click on iOS Safari by targeting iframe parent --- .../Views/Shared/Bitcoin_Lightning_LikeMethodCheckout.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/Views/Shared/Bitcoin_Lightning_LikeMethodCheckout.cshtml b/BTCPayServer/Views/Shared/Bitcoin_Lightning_LikeMethodCheckout.cshtml index 405c17c48..e9db99e9e 100644 --- a/BTCPayServer/Views/Shared/Bitcoin_Lightning_LikeMethodCheckout.cshtml +++ b/BTCPayServer/Views/Shared/Bitcoin_Lightning_LikeMethodCheckout.cshtml @@ -27,7 +27,7 @@
    From 304caaaf1ddf081ac1b908d2c2ecdd02b50f9b85 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sun, 1 Mar 2020 22:38:40 -0600 Subject: [PATCH 242/810] Fixing Selenium tests failing because of dynamic hidden elements --- BTCPayServer.Tests/CheckoutUITests.cs | 58 +++++++++++++++++++-------- 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/BTCPayServer.Tests/CheckoutUITests.cs b/BTCPayServer.Tests/CheckoutUITests.cs index fc7730e18..46531aea9 100644 --- a/BTCPayServer.Tests/CheckoutUITests.cs +++ b/BTCPayServer.Tests/CheckoutUITests.cs @@ -71,7 +71,7 @@ namespace BTCPayServer.Tests } catch { } - s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); + s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); s.Driver.Navigate().Refresh(); s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput")); } @@ -92,7 +92,7 @@ namespace BTCPayServer.Tests s.GoToInvoiceCheckout(invoiceId); Assert.True(s.Driver.FindElement(By.Id("DefaultLang")).FindElements(By.TagName("option")).Count > 1); var payWithTextEnglish = s.Driver.FindElement(By.Id("pay-with-text")).Text; - + var prettyDropdown = s.Driver.FindElement(By.Id("prettydropdown-DefaultLang")); prettyDropdown.Click(); await Task.Delay(200); @@ -100,13 +100,13 @@ namespace BTCPayServer.Tests await Task.Delay(1000); Assert.NotEqual(payWithTextEnglish, s.Driver.FindElement(By.Id("pay-with-text")).Text); s.Driver.Navigate().GoToUrl(s.Driver.Url + "?lang=da-DK"); - + Assert.NotEqual(payWithTextEnglish, s.Driver.FindElement(By.Id("pay-with-text")).Text); - + s.Driver.Quit(); } } - + [Fact(Timeout = TestTimeout)] [Trait("Altcoins", "Altcoins")] [Trait("Lightning", "Lightning")] @@ -121,7 +121,7 @@ namespace BTCPayServer.Tests s.RegisterNewUser(); var store = s.CreateNewStore(); s.AddDerivationScheme("BTC"); - + //check that there is no dropdown since only one payment method is set var invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com"); s.GoToInvoiceCheckout(invoiceId); @@ -129,33 +129,31 @@ namespace BTCPayServer.Tests s.GoToHome(); s.GoToStore(store.storeId); s.AddDerivationScheme("LTC"); - s.AddLightningNode("BTC",LightningConnectionType.CLightning); + s.AddLightningNode("BTC", LightningConnectionType.CLightning); //there should be three now invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com"); s.GoToInvoiceCheckout(invoiceId); - var currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies")); + var currencyDropdownButton = s.Driver.WaitForElement(By.ClassName("payment__currencies")); Assert.Contains("BTC", currencyDropdownButton.Text); currencyDropdownButton.Click(); - + var elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem")); Assert.Equal(3, elements.Count); elements.Single(element => element.Text.Contains("LTC")).Click(); - Thread.Sleep(1000); - currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies")); + currencyDropdownButton = s.Driver.WaitForElement(By.ClassName("payment__currencies")); Assert.Contains("LTC", currencyDropdownButton.Text); currencyDropdownButton.Click(); elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem")); elements.Single(element => element.Text.Contains("Lightning")).Click(); - Thread.Sleep(1000); - currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies")); + currencyDropdownButton = s.Driver.WaitForElement(By.ClassName("payment__currencies")); Assert.Contains("Lightning", currencyDropdownButton.Text); - + s.Driver.Quit(); } } - + [Fact(Timeout = TestTimeout)] [Trait("Lightning", "Lightning")] public async Task CanUseLightningSatsFeature() @@ -171,12 +169,12 @@ namespace BTCPayServer.Tests s.GoToStore(store.storeId, StoreNavPages.Checkout); s.SetCheckbox(s, "LightningAmountInSatoshi", true); var command = s.Driver.FindElement(By.Name("command")); - + command.ForceClick(); var invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com"); s.GoToInvoiceCheckout(invoiceId); Assert.Contains("Sats", s.Driver.FindElement(By.ClassName("payment__currencies_noborder")).Text); - + } } @@ -218,4 +216,30 @@ namespace BTCPayServer.Tests } } } + + public static class SeleniumExtensions + { + /// + /// Utility method to wait until timeout for element to be present (optionally displayed) + /// + /// Wait context + /// How we search for element + /// Flag to wait for element to be displayed or just present + /// How long to wait for element to be present/displayed + /// Element we were waiting for + public static IWebElement WaitForElement(this IWebDriver context, By by, bool displayed = true, uint timeout = 3) + { + var wait = new DefaultWait(context); + wait.Timeout = TimeSpan.FromSeconds(timeout); + wait.IgnoreExceptionTypes(typeof(NoSuchElementException)); + return wait.Until(ctx => + { + var elem = ctx.FindElement(by); + if (displayed && !elem.Displayed) + return null; + + return elem; + }); + } + } } From 8046872315addbc74570ef7c464fc10ea455dc95 Mon Sep 17 00:00:00 2001 From: pavlenex Date: Mon, 2 Mar 2020 10:07:50 +0100 Subject: [PATCH 243/810] Add private info note, change command --- .github/ISSUE_TEMPLATE/bug_report.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 4f99415e6..f9c04659e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -14,10 +14,8 @@ A clear and concise description of what the bug is. **Logs (if applicable)** Basic logs can be found in Server Settings > Logs. -**Profile Configuration** -If you're reporting a deployment issue, paste your configuration: - -`cat /etc/profile.d/btcpay-env.sh` +**Setup Parameters** +If you're reporting a deployment issue run `. btcpay-setup.sh -i` and paste your the paremeters by obscuring private information. **To Reproduce** Steps to reproduce the behavior: From e0aad34105d942d70f7f420c7e69702589d7bc40 Mon Sep 17 00:00:00 2001 From: pavlenex Date: Tue, 3 Mar 2020 22:44:58 +0100 Subject: [PATCH 244/810] Update .Net Core to 3.1, Add MacOS video --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 014d48447..985e459cc 100644 --- a/README.md +++ b/README.md @@ -81,16 +81,17 @@ Contributors looking to do something a bit more challenging, before opening a pu - [Setting up development environment on Windows](https://www.youtube.com/watch?v=ZePbMPSIvHM) - [Setting up development environment Linux (Ubuntu)](https://www.youtube.com/watch?v=j486T_Rk-yw&t) +- [Setting up development environment MacOS](https://www.youtube.com/watch?v=GWR_CcMsEV0) You also have an awesome video of our contributors which explains how to get started.[![Rockstar Dev and Britt Kelly - Btc Pay Server Code Along](https://img.youtube.com/vi/ZePbMPSIvHM/sddefault.jpg)](https://www.youtube.com/embed/VNMnd-dX9Q8) -Here is some info about [how to extend the themes](Docs/Themes.md) +Here is some info about [how to extend the themes](https://github.com/btcpayserver/btcpayserver-doc/blob/master/Theme.md) ## How to build -While the documentation advises to use docker-compose, you may want to build BTCPay yourself. +While the documentation advises to use docker-compose, you may want to build BTCPay Server yourself. -First install .NET Core SDK v2.1.9 as specified by [Microsoft website](https://www.microsoft.com/net/download/dotnet-core/2.1). +First install .NET Core SDK v3.1 as specified by [Microsoft website](https://dotnet.microsoft.com/download/dotnet-core/3.1). On Powershell: ``` @@ -118,7 +119,7 @@ On linux: ## How to debug -If you want to debug, use Visual Studio Code or Visual Studio 2017. +If you want to debug, use Visual Studio Code or Visual Studio 2019. You need to run the development time docker-compose as described [in the test guide](BTCPayServer.Tests/README.md). From d9bdb46033b2d30c9b5170f7e229638b15641c0b Mon Sep 17 00:00:00 2001 From: Umar Bolatov Date: Tue, 3 Mar 2020 21:15:27 -0800 Subject: [PATCH 245/810] Prettify wallet receive tab screen --- .../Views/Wallets/WalletReceive.cshtml | 65 ++++++++++--------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/BTCPayServer/Views/Wallets/WalletReceive.cshtml b/BTCPayServer/Views/Wallets/WalletReceive.cshtml index 44e3a5c55..b342f529f 100644 --- a/BTCPayServer/Views/Wallets/WalletReceive.cshtml +++ b/BTCPayServer/Views/Wallets/WalletReceive.cshtml @@ -14,21 +14,39 @@
    }
    -
    +
    - @if (string.IsNullOrEmpty(Model.Address)) - { +
    + @if (string.IsNullOrEmpty(Model.Address)) + { -

    Receive @Model.CryptoCode

    - - } - else - { -

    Next available @Model.CryptoCode address

    -
    +
    + + Sponsor BTSE + +
    + BTSE +
    +
    Sponsor DG lab diff --git a/BTCPayServer/wwwroot/img/btse.svg b/BTCPayServer/wwwroot/img/btse.svg new file mode 100644 index 000000000..f1f7c409d --- /dev/null +++ b/BTCPayServer/wwwroot/img/btse.svg @@ -0,0 +1,16 @@ + + + + logo_square_bluebg + Created with Sketch. + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 985e459cc..b0b381624 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,13 @@ The BTCPay Server Project is proudly supported by these entities through the [BT Square Crypto + + + BTSE +
    + BTSE +
    + DG Lab From 6f6e8ba1a1e41d7a933d30b02959127b3cd4c35e Mon Sep 17 00:00:00 2001 From: Kukks Date: Tue, 10 Mar 2020 11:20:05 +0100 Subject: [PATCH 255/810] fix duplicate key error in crowdfunding --- BTCPayServer/Services/Apps/AppService.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/BTCPayServer/Services/Apps/AppService.cs b/BTCPayServer/Services/Apps/AppService.cs index 75e28a6e4..209bd157c 100644 --- a/BTCPayServer/Services/Apps/AppService.cs +++ b/BTCPayServer/Services/Apps/AppService.cs @@ -28,6 +28,7 @@ using NBitcoin; using NBitcoin.DataEncoders; using NBitpayClient; using Newtonsoft.Json.Linq; +using NUglify.Helpers; using YamlDotNet.RepresentationModel; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -154,8 +155,9 @@ namespace BTCPayServer.Services.Apps AnimationColors = settings.AnimationColors, CurrencyData = _Currencies.GetCurrencyData(settings.TargetCurrency, true), CurrencyDataPayments = currentPayments.Select(pair => pair.Key) - .Concat(pendingPayments.Select(pair => pair.Key)).Distinct() + .Concat(pendingPayments.Select(pair => pair.Key)) .Select(id => _Currencies.GetCurrencyData(id.CryptoCode, true)) + .DistinctBy(data => data.Code) .ToDictionary(data => data.Code, data => data), Info = new ViewCrowdfundViewModel.CrowdfundInfo() { From a9bf843be05a5400eaea1acc5d09349dd14debaf Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 10 Mar 2020 21:23:59 +0900 Subject: [PATCH 256/810] Bump various libraries --- BTCPayServer.Common/BTCPayServer.Common.csproj | 2 +- BTCPayServer.Data/BTCPayServer.Data.csproj | 4 ++-- BTCPayServer.Tests/BTCPayServer.Tests.csproj | 4 ++-- BTCPayServer.Tests/docker-compose.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/BTCPayServer.Common/BTCPayServer.Common.csproj b/BTCPayServer.Common/BTCPayServer.Common.csproj index 94514cc10..6fd66c12c 100644 --- a/BTCPayServer.Common/BTCPayServer.Common.csproj +++ b/BTCPayServer.Common/BTCPayServer.Common.csproj @@ -4,6 +4,6 @@ - + diff --git a/BTCPayServer.Data/BTCPayServer.Data.csproj b/BTCPayServer.Data/BTCPayServer.Data.csproj index a8612e9cf..00bfa3e97 100644 --- a/BTCPayServer.Data/BTCPayServer.Data.csproj +++ b/BTCPayServer.Data/BTCPayServer.Data.csproj @@ -5,8 +5,8 @@ - - + + diff --git a/BTCPayServer.Tests/BTCPayServer.Tests.csproj b/BTCPayServer.Tests/BTCPayServer.Tests.csproj index 68dcbfc74..689f2ed40 100644 --- a/BTCPayServer.Tests/BTCPayServer.Tests.csproj +++ b/BTCPayServer.Tests/BTCPayServer.Tests.csproj @@ -23,9 +23,9 @@ - + - + all diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index f1010a58e..ebfb469ac 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -76,7 +76,7 @@ services: - customer_lnd - merchant_lnd nbxplorer: - image: nicolasdorier/nbxplorer:2.1.8 + image: nicolasdorier/nbxplorer:2.1.13 restart: unless-stopped ports: - "32838:32838" From 691a8d6fd85663a31e112b2bf2beba6ba5195f99 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 10 Mar 2020 21:28:00 +0900 Subject: [PATCH 257/810] Fix warnings --- BTCPayServer/Controllers/ManageController.APIKeys.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BTCPayServer/Controllers/ManageController.APIKeys.cs b/BTCPayServer/Controllers/ManageController.APIKeys.cs index 23052fe9b..9db9e31f7 100644 --- a/BTCPayServer/Controllers/ManageController.APIKeys.cs +++ b/BTCPayServer/Controllers/ManageController.APIKeys.cs @@ -40,10 +40,10 @@ namespace BTCPayServer.Controllers } return View("Confirm", new ConfirmModel() { - Title = "Delete API Key "+ ( string.IsNullOrEmpty(key.Label)? string.Empty: key.Label) + "("+key.Id+")", + Title = "Delete API Key " + (string.IsNullOrEmpty(key.Label) ? string.Empty : key.Label) + "(" + key.Id + ")", Description = "Any application using this api key will immediately lose access", Action = "Delete", - ActionUrl = Request.GetCurrentUrl().Replace("RemoveAPIKey", "RemoveAPIKeyPost") + ActionUrl = this.Url.ActionLink(nameof(RemoveAPIKeyPost), values: new { id = id }) }); } @@ -247,7 +247,7 @@ namespace BTCPayServer.Controllers { var key = new APIKeyData() { - Id = Guid.NewGuid().ToString().Replace("-", string.Empty), + Id = Guid.NewGuid().ToString().Replace("-", string.Empty, StringComparison.OrdinalIgnoreCase), Type = APIKeyType.Permanent, UserId = _userManager.GetUserId(User), Label = viewModel.Label From 153992a458a1de35251a989b2f079307574c21d6 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 10 Mar 2020 21:30:46 +0900 Subject: [PATCH 258/810] Use good rng for generating API keys --- BTCPayServer/Controllers/ManageController.APIKeys.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/BTCPayServer/Controllers/ManageController.APIKeys.cs b/BTCPayServer/Controllers/ManageController.APIKeys.cs index 9db9e31f7..b5abfbb0e 100644 --- a/BTCPayServer/Controllers/ManageController.APIKeys.cs +++ b/BTCPayServer/Controllers/ManageController.APIKeys.cs @@ -10,6 +10,8 @@ using BTCPayServer.Security; using BTCPayServer.Security.APIKeys; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using NBitcoin; +using NBitcoin.DataEncoders; using NSwag.Annotations; namespace BTCPayServer.Controllers @@ -247,7 +249,7 @@ namespace BTCPayServer.Controllers { var key = new APIKeyData() { - Id = Guid.NewGuid().ToString().Replace("-", string.Empty, StringComparison.OrdinalIgnoreCase), + Id = Encoders.Hex.EncodeData(RandomUtils.GetBytes(20)), Type = APIKeyType.Permanent, UserId = _userManager.GetUserId(User), Label = viewModel.Label From 3cdf8814386882e6c638d8cb5cafe7c362b1d3e3 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 10 Mar 2020 22:31:05 +0900 Subject: [PATCH 259/810] bump lightning libraries --- BTCPayServer/BTCPayServer.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index c99a79948..68d803f81 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -30,7 +30,7 @@ - + From 67befcc6296df80589285d10898780356b3f9cba Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 10 Mar 2020 22:48:17 +0900 Subject: [PATCH 260/810] bump --- Build/Version.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Build/Version.csproj b/Build/Version.csproj index c385145f5..566c5bb79 100644 --- a/Build/Version.csproj +++ b/Build/Version.csproj @@ -1,5 +1,5 @@ - 1.0.3.163 + 1.0.3.164 From 42152050a3786bd3db103220e1a45b60c9fcddaf Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Wed, 11 Mar 2020 20:46:37 +0900 Subject: [PATCH 261/810] Simplify RBF handling, and handle case of double spend happening outside of wallet (Fix #1375) --- BTCPayServer.Tests/UnitTest1.cs | 39 +++++ .../Payments/Bitcoin/NBXplorerListener.cs | 134 +++++++----------- 2 files changed, 87 insertions(+), 86 deletions(-) diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 5fd023d7d..c739427ba 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -1101,6 +1101,45 @@ namespace BTCPayServer.Tests Assert.Equal(payment2, invoice.BtcPaid); Assert.Equal("False", invoice.ExceptionStatus.ToString()); }); + + + Logs.Tester.LogInformation($"Let's test out rbf payments where the payment gets sent elsehwere instead"); + var invoice2 = user.BitPay.CreateInvoice(new Invoice() + { + Price = 0.01m, + Currency = "BTC" + }, Facade.Merchant); + + var invoice2Address = BitcoinAddress.Create(invoice2.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork); + uint256 invoice2tx1Id = await tester.ExplorerNode.SendToAddressAsync(invoice2Address, invoice2.BtcDue, replaceable: true); + Transaction invoice2Tx1 = null; + TestUtils.Eventually(() => + { + invoice2 = user.BitPay.GetInvoice(invoice2.Id); + Assert.Equal("paid", invoice2.Status); + invoice2Tx1 = tester.ExplorerNode.GetRawTransaction(new uint256(invoice2tx1Id)); + }); + var invoice2Tx2 = invoice2Tx1.Clone(); + foreach (var input in invoice2Tx2.Inputs) + { + input.ScriptSig = Script.Empty; //Strip signatures + input.WitScript = WitScript.Empty; //Strip signatures + } + + output = invoice2Tx2.Outputs.First(o => + o.ScriptPubKey == invoice2Address.ScriptPubKey); + output.Value -= new Money(10_000, MoneyUnit.Satoshi); + output.ScriptPubKey = new Key().ScriptPubKey; + invoice2Tx2 = await tester.ExplorerNode.SignRawTransactionAsync(invoice2Tx2); + await tester.ExplorerNode.SendRawTransactionAsync(invoice2Tx2); + tester.ExplorerNode.Generate(1); + await TestUtils.EventuallyAsync(async () => + { + var i = await tester.PayTester.InvoiceRepository.GetInvoice(invoice2.Id); + Assert.Equal(InvoiceStatus.New, i.Status); + Assert.Single(i.GetPayments()); + Assert.False(i.GetPayments().First().Accounted); + }); } } diff --git a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs index eb4fc5cd5..72f59bed5 100644 --- a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs +++ b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs @@ -19,6 +19,7 @@ using NBXplorer.Models; using BTCPayServer.Payments; using BTCPayServer.HostedServices; using NBitcoin.Altcoins.Elements; +using NBitcoin.RPC; namespace BTCPayServer.Payments.Bitcoin { @@ -38,7 +39,7 @@ namespace BTCPayServer.Payments.Bitcoin public NBXplorerListener(ExplorerClientProvider explorerClients, BTCPayWalletProvider wallets, InvoiceRepository invoiceRepository, - EventAggregator aggregator, + EventAggregator aggregator, IHostApplicationLifetime lifetime) { PollInterval = TimeSpan.FromMinutes(1.0); @@ -150,28 +151,28 @@ namespace BTCPayServer.Payments.Bitcoin CryptoCode = wallet.Network.CryptoCode, NewTransactionEvent = evt }); - foreach (var output in network.GetValidOutputs(evt)) + foreach (var output in network.GetValidOutputs(evt)) { - var key = output.Item1.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant(); - var invoice = (await _InvoiceRepository.GetInvoicesFromAddresses(new [] {key})).FirstOrDefault(); - if (invoice != null) + var key = output.Item1.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant(); + var invoice = (await _InvoiceRepository.GetInvoicesFromAddresses(new[] { key })).FirstOrDefault(); + if (invoice != null) + { + var address = network.NBXplorerNetwork.CreateAddress(evt.DerivationStrategy, + output.Item1.KeyPath, output.Item1.ScriptPubKey); + var paymentData = new BitcoinLikePaymentData(address, output.matchedOutput.Value, output.outPoint, evt.TransactionData.Transaction.RBF); + var alreadyExist = GetAllBitcoinPaymentData(invoice).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any(); + if (!alreadyExist) { - var address = network.NBXplorerNetwork.CreateAddress(evt.DerivationStrategy, - output.Item1.KeyPath, output.Item1.ScriptPubKey); - var paymentData = new BitcoinLikePaymentData(address, output.matchedOutput.Value, output.outPoint, evt.TransactionData.Transaction.RBF); - var alreadyExist = GetAllBitcoinPaymentData(invoice).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any(); - if (!alreadyExist) - { - var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network); - if(payment != null) - await ReceivedPayment(wallet, invoice, payment, evt.DerivationStrategy); - } - else - { - await UpdatePaymentStates(wallet, invoice.Id); - } + var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network); + if (payment != null) + await ReceivedPayment(wallet, invoice, payment, evt.DerivationStrategy); } - + else + { + await UpdatePaymentStates(wallet, invoice.Id); + } + } + } break; default: @@ -216,7 +217,6 @@ namespace BTCPayServer.Payments.Bitcoin var transactions = await wallet.GetTransactions(GetAllBitcoinPaymentData(invoice) .Select(p => p.Outpoint.Hash) .ToArray()); - var conflicts = GetConflicts(transactions.Select(t => t.Value)); foreach (var payment in invoice.GetPayments(wallet.Network)) { if (payment.GetPaymentMethodId().PaymentType != PaymentTypes.BTCLike) @@ -225,8 +225,29 @@ namespace BTCPayServer.Payments.Bitcoin if (!transactions.TryGetValue(paymentData.Outpoint.Hash, out TransactionResult tx)) continue; var txId = tx.Transaction.GetHash(); - var txConflict = conflicts.GetConflict(txId); - var accounted = txConflict == null || txConflict.IsWinner(txId); + bool accounted = true; + if (tx.Confirmations == 0) + { + // Let's check if it was orphaned by broadcasting it again + var explorerClient = _ExplorerClients.GetExplorerClient(wallet.Network); + try + { + var result = await explorerClient.BroadcastAsync(tx.Transaction, _Cts.Token); + accounted = result.Success || + result.RPCCode == RPCErrorCode.RPC_TRANSACTION_ALREADY_IN_CHAIN || + !( + // Happen if a blocks mined a replacement + // Or if the tx is a double spend of something already in the mempool without rbf + result.RPCCode == RPCErrorCode.RPC_TRANSACTION_ERROR || + // Happen if RBF is on and fee insufficient + result.RPCCode == RPCErrorCode.RPC_TRANSACTION_REJECTED); + } + // RPC might be unavailable, we can't check double spend so let's assume there is none + catch + { + + } + } bool updated = false; if (accounted != payment.Accounted) @@ -258,65 +279,6 @@ namespace BTCPayServer.Payments.Bitcoin return invoice; } - class TransactionConflict - { - public Dictionary Transactions { get; set; } = new Dictionary(); - - - uint256 _Winner; - public bool IsWinner(uint256 txId) - { - if (_Winner == null) - { - var confirmed = Transactions.FirstOrDefault(t => t.Value.Confirmations >= 1); - if (!confirmed.Equals(default(KeyValuePair))) - { - _Winner = confirmed.Key; - } - else - { - // Take the most recent (bitcoin node would not forward a conflict without a successful RBF) - _Winner = Transactions - .OrderByDescending(t => t.Value.Timestamp) - .First() - .Key; - } - } - return _Winner == txId; - } - } - class TransactionConflicts : List - { - public TransactionConflicts(IEnumerable collection) : base(collection) - { - - } - - public TransactionConflict GetConflict(uint256 txId) - { - return this.FirstOrDefault(c => c.Transactions.ContainsKey(txId)); - } - } - private TransactionConflicts GetConflicts(IEnumerable transactions) - { - Dictionary conflictsByOutpoint = new Dictionary(); - foreach (var tx in transactions) - { - var hash = tx.Transaction.GetHash(); - foreach (var input in tx.Transaction.Inputs) - { - TransactionConflict conflict = new TransactionConflict(); - if (!conflictsByOutpoint.TryAdd(input.PrevOut, conflict)) - { - conflict = conflictsByOutpoint[input.PrevOut]; - } - if (!conflict.Transactions.ContainsKey(hash)) - conflict.Transactions.Add(hash, tx); - } - } - return new TransactionConflicts(conflictsByOutpoint.Where(c => c.Value.Transactions.Count > 1).Select(c => c.Value)); - } - private async Task FindPaymentViaPolling(BTCPayWallet wallet, BTCPayNetwork network) { int totalPayment = 0; @@ -339,17 +301,17 @@ namespace BTCPayServer.Payments.Bitcoin foreach (var coin in coins.Where(c => !alreadyAccounted.Contains(c.OutPoint))) { var transaction = await wallet.GetTransactionAsync(coin.OutPoint.Hash); - + var address = network.NBXplorerNetwork.CreateAddress(strategy, coin.KeyPath, coin.ScriptPubKey); var paymentData = new BitcoinLikePaymentData(address, coin.Value, coin.OutPoint, transaction.Transaction.RBF); - + var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network).ConfigureAwait(false); alreadyAccounted.Add(coin.OutPoint); if (payment != null) { invoice = await ReceivedPayment(wallet, invoice, payment, strategy); - if(invoice == null) + if (invoice == null) continue; totalPayment++; } @@ -385,7 +347,7 @@ namespace BTCPayServer.Payments.Bitcoin invoice.SetPaymentMethod(paymentMethod); } wallet.InvalidateCache(strategy); - _Aggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment){Payment = payment}); + _Aggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment }); return invoice; } public async Task StopAsync(CancellationToken cancellationToken) From 6bf7ef07988b5fcf23450a436338411adb0bff58 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Wed, 11 Mar 2020 20:57:19 +0900 Subject: [PATCH 262/810] Revert "Simplify RBF handling, and handle case of double spend happening outside of wallet (Fix #1375)" This reverts commit 42152050a3786bd3db103220e1a45b60c9fcddaf. --- BTCPayServer.Tests/UnitTest1.cs | 39 ----- .../Payments/Bitcoin/NBXplorerListener.cs | 134 +++++++++++------- 2 files changed, 86 insertions(+), 87 deletions(-) diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index c739427ba..5fd023d7d 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -1101,45 +1101,6 @@ namespace BTCPayServer.Tests Assert.Equal(payment2, invoice.BtcPaid); Assert.Equal("False", invoice.ExceptionStatus.ToString()); }); - - - Logs.Tester.LogInformation($"Let's test out rbf payments where the payment gets sent elsehwere instead"); - var invoice2 = user.BitPay.CreateInvoice(new Invoice() - { - Price = 0.01m, - Currency = "BTC" - }, Facade.Merchant); - - var invoice2Address = BitcoinAddress.Create(invoice2.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork); - uint256 invoice2tx1Id = await tester.ExplorerNode.SendToAddressAsync(invoice2Address, invoice2.BtcDue, replaceable: true); - Transaction invoice2Tx1 = null; - TestUtils.Eventually(() => - { - invoice2 = user.BitPay.GetInvoice(invoice2.Id); - Assert.Equal("paid", invoice2.Status); - invoice2Tx1 = tester.ExplorerNode.GetRawTransaction(new uint256(invoice2tx1Id)); - }); - var invoice2Tx2 = invoice2Tx1.Clone(); - foreach (var input in invoice2Tx2.Inputs) - { - input.ScriptSig = Script.Empty; //Strip signatures - input.WitScript = WitScript.Empty; //Strip signatures - } - - output = invoice2Tx2.Outputs.First(o => - o.ScriptPubKey == invoice2Address.ScriptPubKey); - output.Value -= new Money(10_000, MoneyUnit.Satoshi); - output.ScriptPubKey = new Key().ScriptPubKey; - invoice2Tx2 = await tester.ExplorerNode.SignRawTransactionAsync(invoice2Tx2); - await tester.ExplorerNode.SendRawTransactionAsync(invoice2Tx2); - tester.ExplorerNode.Generate(1); - await TestUtils.EventuallyAsync(async () => - { - var i = await tester.PayTester.InvoiceRepository.GetInvoice(invoice2.Id); - Assert.Equal(InvoiceStatus.New, i.Status); - Assert.Single(i.GetPayments()); - Assert.False(i.GetPayments().First().Accounted); - }); } } diff --git a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs index 72f59bed5..eb4fc5cd5 100644 --- a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs +++ b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs @@ -19,7 +19,6 @@ using NBXplorer.Models; using BTCPayServer.Payments; using BTCPayServer.HostedServices; using NBitcoin.Altcoins.Elements; -using NBitcoin.RPC; namespace BTCPayServer.Payments.Bitcoin { @@ -39,7 +38,7 @@ namespace BTCPayServer.Payments.Bitcoin public NBXplorerListener(ExplorerClientProvider explorerClients, BTCPayWalletProvider wallets, InvoiceRepository invoiceRepository, - EventAggregator aggregator, + EventAggregator aggregator, IHostApplicationLifetime lifetime) { PollInterval = TimeSpan.FromMinutes(1.0); @@ -151,28 +150,28 @@ namespace BTCPayServer.Payments.Bitcoin CryptoCode = wallet.Network.CryptoCode, NewTransactionEvent = evt }); - foreach (var output in network.GetValidOutputs(evt)) + foreach (var output in network.GetValidOutputs(evt)) { - var key = output.Item1.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant(); - var invoice = (await _InvoiceRepository.GetInvoicesFromAddresses(new[] { key })).FirstOrDefault(); - if (invoice != null) - { - var address = network.NBXplorerNetwork.CreateAddress(evt.DerivationStrategy, - output.Item1.KeyPath, output.Item1.ScriptPubKey); - var paymentData = new BitcoinLikePaymentData(address, output.matchedOutput.Value, output.outPoint, evt.TransactionData.Transaction.RBF); - var alreadyExist = GetAllBitcoinPaymentData(invoice).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any(); - if (!alreadyExist) + var key = output.Item1.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant(); + var invoice = (await _InvoiceRepository.GetInvoicesFromAddresses(new [] {key})).FirstOrDefault(); + if (invoice != null) { - var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network); - if (payment != null) - await ReceivedPayment(wallet, invoice, payment, evt.DerivationStrategy); + var address = network.NBXplorerNetwork.CreateAddress(evt.DerivationStrategy, + output.Item1.KeyPath, output.Item1.ScriptPubKey); + var paymentData = new BitcoinLikePaymentData(address, output.matchedOutput.Value, output.outPoint, evt.TransactionData.Transaction.RBF); + var alreadyExist = GetAllBitcoinPaymentData(invoice).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any(); + if (!alreadyExist) + { + var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network); + if(payment != null) + await ReceivedPayment(wallet, invoice, payment, evt.DerivationStrategy); + } + else + { + await UpdatePaymentStates(wallet, invoice.Id); + } } - else - { - await UpdatePaymentStates(wallet, invoice.Id); - } - } - + } break; default: @@ -217,6 +216,7 @@ namespace BTCPayServer.Payments.Bitcoin var transactions = await wallet.GetTransactions(GetAllBitcoinPaymentData(invoice) .Select(p => p.Outpoint.Hash) .ToArray()); + var conflicts = GetConflicts(transactions.Select(t => t.Value)); foreach (var payment in invoice.GetPayments(wallet.Network)) { if (payment.GetPaymentMethodId().PaymentType != PaymentTypes.BTCLike) @@ -225,29 +225,8 @@ namespace BTCPayServer.Payments.Bitcoin if (!transactions.TryGetValue(paymentData.Outpoint.Hash, out TransactionResult tx)) continue; var txId = tx.Transaction.GetHash(); - bool accounted = true; - if (tx.Confirmations == 0) - { - // Let's check if it was orphaned by broadcasting it again - var explorerClient = _ExplorerClients.GetExplorerClient(wallet.Network); - try - { - var result = await explorerClient.BroadcastAsync(tx.Transaction, _Cts.Token); - accounted = result.Success || - result.RPCCode == RPCErrorCode.RPC_TRANSACTION_ALREADY_IN_CHAIN || - !( - // Happen if a blocks mined a replacement - // Or if the tx is a double spend of something already in the mempool without rbf - result.RPCCode == RPCErrorCode.RPC_TRANSACTION_ERROR || - // Happen if RBF is on and fee insufficient - result.RPCCode == RPCErrorCode.RPC_TRANSACTION_REJECTED); - } - // RPC might be unavailable, we can't check double spend so let's assume there is none - catch - { - - } - } + var txConflict = conflicts.GetConflict(txId); + var accounted = txConflict == null || txConflict.IsWinner(txId); bool updated = false; if (accounted != payment.Accounted) @@ -279,6 +258,65 @@ namespace BTCPayServer.Payments.Bitcoin return invoice; } + class TransactionConflict + { + public Dictionary Transactions { get; set; } = new Dictionary(); + + + uint256 _Winner; + public bool IsWinner(uint256 txId) + { + if (_Winner == null) + { + var confirmed = Transactions.FirstOrDefault(t => t.Value.Confirmations >= 1); + if (!confirmed.Equals(default(KeyValuePair))) + { + _Winner = confirmed.Key; + } + else + { + // Take the most recent (bitcoin node would not forward a conflict without a successful RBF) + _Winner = Transactions + .OrderByDescending(t => t.Value.Timestamp) + .First() + .Key; + } + } + return _Winner == txId; + } + } + class TransactionConflicts : List + { + public TransactionConflicts(IEnumerable collection) : base(collection) + { + + } + + public TransactionConflict GetConflict(uint256 txId) + { + return this.FirstOrDefault(c => c.Transactions.ContainsKey(txId)); + } + } + private TransactionConflicts GetConflicts(IEnumerable transactions) + { + Dictionary conflictsByOutpoint = new Dictionary(); + foreach (var tx in transactions) + { + var hash = tx.Transaction.GetHash(); + foreach (var input in tx.Transaction.Inputs) + { + TransactionConflict conflict = new TransactionConflict(); + if (!conflictsByOutpoint.TryAdd(input.PrevOut, conflict)) + { + conflict = conflictsByOutpoint[input.PrevOut]; + } + if (!conflict.Transactions.ContainsKey(hash)) + conflict.Transactions.Add(hash, tx); + } + } + return new TransactionConflicts(conflictsByOutpoint.Where(c => c.Value.Transactions.Count > 1).Select(c => c.Value)); + } + private async Task FindPaymentViaPolling(BTCPayWallet wallet, BTCPayNetwork network) { int totalPayment = 0; @@ -301,17 +339,17 @@ namespace BTCPayServer.Payments.Bitcoin foreach (var coin in coins.Where(c => !alreadyAccounted.Contains(c.OutPoint))) { var transaction = await wallet.GetTransactionAsync(coin.OutPoint.Hash); - + var address = network.NBXplorerNetwork.CreateAddress(strategy, coin.KeyPath, coin.ScriptPubKey); var paymentData = new BitcoinLikePaymentData(address, coin.Value, coin.OutPoint, transaction.Transaction.RBF); - + var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network).ConfigureAwait(false); alreadyAccounted.Add(coin.OutPoint); if (payment != null) { invoice = await ReceivedPayment(wallet, invoice, payment, strategy); - if (invoice == null) + if(invoice == null) continue; totalPayment++; } @@ -347,7 +385,7 @@ namespace BTCPayServer.Payments.Bitcoin invoice.SetPaymentMethod(paymentMethod); } wallet.InvalidateCache(strategy); - _Aggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment }); + _Aggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment){Payment = payment}); return invoice; } public async Task StopAsync(CancellationToken cancellationToken) From 95f859b6db1279cb54254c5c5823f521c78052bd Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Wed, 11 Mar 2020 20:46:37 +0900 Subject: [PATCH 263/810] Simplify RBF handling, and handle case of double spend happening outside of wallet (Fix #1375) --- BTCPayServer.Tests/UnitTest1.cs | 39 +++++ .../Payments/Bitcoin/NBXplorerListener.cs | 138 +++++++----------- 2 files changed, 91 insertions(+), 86 deletions(-) diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 5fd023d7d..c739427ba 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -1101,6 +1101,45 @@ namespace BTCPayServer.Tests Assert.Equal(payment2, invoice.BtcPaid); Assert.Equal("False", invoice.ExceptionStatus.ToString()); }); + + + Logs.Tester.LogInformation($"Let's test out rbf payments where the payment gets sent elsehwere instead"); + var invoice2 = user.BitPay.CreateInvoice(new Invoice() + { + Price = 0.01m, + Currency = "BTC" + }, Facade.Merchant); + + var invoice2Address = BitcoinAddress.Create(invoice2.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork); + uint256 invoice2tx1Id = await tester.ExplorerNode.SendToAddressAsync(invoice2Address, invoice2.BtcDue, replaceable: true); + Transaction invoice2Tx1 = null; + TestUtils.Eventually(() => + { + invoice2 = user.BitPay.GetInvoice(invoice2.Id); + Assert.Equal("paid", invoice2.Status); + invoice2Tx1 = tester.ExplorerNode.GetRawTransaction(new uint256(invoice2tx1Id)); + }); + var invoice2Tx2 = invoice2Tx1.Clone(); + foreach (var input in invoice2Tx2.Inputs) + { + input.ScriptSig = Script.Empty; //Strip signatures + input.WitScript = WitScript.Empty; //Strip signatures + } + + output = invoice2Tx2.Outputs.First(o => + o.ScriptPubKey == invoice2Address.ScriptPubKey); + output.Value -= new Money(10_000, MoneyUnit.Satoshi); + output.ScriptPubKey = new Key().ScriptPubKey; + invoice2Tx2 = await tester.ExplorerNode.SignRawTransactionAsync(invoice2Tx2); + await tester.ExplorerNode.SendRawTransactionAsync(invoice2Tx2); + tester.ExplorerNode.Generate(1); + await TestUtils.EventuallyAsync(async () => + { + var i = await tester.PayTester.InvoiceRepository.GetInvoice(invoice2.Id); + Assert.Equal(InvoiceStatus.New, i.Status); + Assert.Single(i.GetPayments()); + Assert.False(i.GetPayments().First().Accounted); + }); } } diff --git a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs index eb4fc5cd5..7f0b82990 100644 --- a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs +++ b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs @@ -19,6 +19,7 @@ using NBXplorer.Models; using BTCPayServer.Payments; using BTCPayServer.HostedServices; using NBitcoin.Altcoins.Elements; +using NBitcoin.RPC; namespace BTCPayServer.Payments.Bitcoin { @@ -38,7 +39,7 @@ namespace BTCPayServer.Payments.Bitcoin public NBXplorerListener(ExplorerClientProvider explorerClients, BTCPayWalletProvider wallets, InvoiceRepository invoiceRepository, - EventAggregator aggregator, + EventAggregator aggregator, IHostApplicationLifetime lifetime) { PollInterval = TimeSpan.FromMinutes(1.0); @@ -150,28 +151,28 @@ namespace BTCPayServer.Payments.Bitcoin CryptoCode = wallet.Network.CryptoCode, NewTransactionEvent = evt }); - foreach (var output in network.GetValidOutputs(evt)) + foreach (var output in network.GetValidOutputs(evt)) { - var key = output.Item1.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant(); - var invoice = (await _InvoiceRepository.GetInvoicesFromAddresses(new [] {key})).FirstOrDefault(); - if (invoice != null) + var key = output.Item1.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant(); + var invoice = (await _InvoiceRepository.GetInvoicesFromAddresses(new[] { key })).FirstOrDefault(); + if (invoice != null) + { + var address = network.NBXplorerNetwork.CreateAddress(evt.DerivationStrategy, + output.Item1.KeyPath, output.Item1.ScriptPubKey); + var paymentData = new BitcoinLikePaymentData(address, output.matchedOutput.Value, output.outPoint, evt.TransactionData.Transaction.RBF); + var alreadyExist = GetAllBitcoinPaymentData(invoice).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any(); + if (!alreadyExist) { - var address = network.NBXplorerNetwork.CreateAddress(evt.DerivationStrategy, - output.Item1.KeyPath, output.Item1.ScriptPubKey); - var paymentData = new BitcoinLikePaymentData(address, output.matchedOutput.Value, output.outPoint, evt.TransactionData.Transaction.RBF); - var alreadyExist = GetAllBitcoinPaymentData(invoice).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any(); - if (!alreadyExist) - { - var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network); - if(payment != null) - await ReceivedPayment(wallet, invoice, payment, evt.DerivationStrategy); - } - else - { - await UpdatePaymentStates(wallet, invoice.Id); - } + var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network); + if (payment != null) + await ReceivedPayment(wallet, invoice, payment, evt.DerivationStrategy); } - + else + { + await UpdatePaymentStates(wallet, invoice.Id); + } + } + } break; default: @@ -216,7 +217,6 @@ namespace BTCPayServer.Payments.Bitcoin var transactions = await wallet.GetTransactions(GetAllBitcoinPaymentData(invoice) .Select(p => p.Outpoint.Hash) .ToArray()); - var conflicts = GetConflicts(transactions.Select(t => t.Value)); foreach (var payment in invoice.GetPayments(wallet.Network)) { if (payment.GetPaymentMethodId().PaymentType != PaymentTypes.BTCLike) @@ -225,8 +225,33 @@ namespace BTCPayServer.Payments.Bitcoin if (!transactions.TryGetValue(paymentData.Outpoint.Hash, out TransactionResult tx)) continue; var txId = tx.Transaction.GetHash(); - var txConflict = conflicts.GetConflict(txId); - var accounted = txConflict == null || txConflict.IsWinner(txId); + bool accounted = true; + if (tx.Confirmations == 0) + { + // Let's check if it was orphaned by broadcasting it again + var explorerClient = _ExplorerClients.GetExplorerClient(wallet.Network); + try + { + var result = await explorerClient.BroadcastAsync(tx.Transaction, _Cts.Token); + accounted = result.Success || + result.RPCCode == RPCErrorCode.RPC_TRANSACTION_ALREADY_IN_CHAIN || + !( + // Happen if a blocks mined a replacement + // Or if the tx is a double spend of something already in the mempool without rbf + result.RPCCode == RPCErrorCode.RPC_TRANSACTION_ERROR || + // Happen if RBF is on and fee insufficient + result.RPCCode == RPCErrorCode.RPC_TRANSACTION_REJECTED); + if (!accounted && payment.Accounted) + { + Logs.PayServer.LogInformation($"{wallet.Network.CryptoCode}: The transaction {tx.TransactionHash} has been replaced."); + } + } + // RPC might be unavailable, we can't check double spend so let's assume there is none + catch + { + + } + } bool updated = false; if (accounted != payment.Accounted) @@ -258,65 +283,6 @@ namespace BTCPayServer.Payments.Bitcoin return invoice; } - class TransactionConflict - { - public Dictionary Transactions { get; set; } = new Dictionary(); - - - uint256 _Winner; - public bool IsWinner(uint256 txId) - { - if (_Winner == null) - { - var confirmed = Transactions.FirstOrDefault(t => t.Value.Confirmations >= 1); - if (!confirmed.Equals(default(KeyValuePair))) - { - _Winner = confirmed.Key; - } - else - { - // Take the most recent (bitcoin node would not forward a conflict without a successful RBF) - _Winner = Transactions - .OrderByDescending(t => t.Value.Timestamp) - .First() - .Key; - } - } - return _Winner == txId; - } - } - class TransactionConflicts : List - { - public TransactionConflicts(IEnumerable collection) : base(collection) - { - - } - - public TransactionConflict GetConflict(uint256 txId) - { - return this.FirstOrDefault(c => c.Transactions.ContainsKey(txId)); - } - } - private TransactionConflicts GetConflicts(IEnumerable transactions) - { - Dictionary conflictsByOutpoint = new Dictionary(); - foreach (var tx in transactions) - { - var hash = tx.Transaction.GetHash(); - foreach (var input in tx.Transaction.Inputs) - { - TransactionConflict conflict = new TransactionConflict(); - if (!conflictsByOutpoint.TryAdd(input.PrevOut, conflict)) - { - conflict = conflictsByOutpoint[input.PrevOut]; - } - if (!conflict.Transactions.ContainsKey(hash)) - conflict.Transactions.Add(hash, tx); - } - } - return new TransactionConflicts(conflictsByOutpoint.Where(c => c.Value.Transactions.Count > 1).Select(c => c.Value)); - } - private async Task FindPaymentViaPolling(BTCPayWallet wallet, BTCPayNetwork network) { int totalPayment = 0; @@ -339,17 +305,17 @@ namespace BTCPayServer.Payments.Bitcoin foreach (var coin in coins.Where(c => !alreadyAccounted.Contains(c.OutPoint))) { var transaction = await wallet.GetTransactionAsync(coin.OutPoint.Hash); - + var address = network.NBXplorerNetwork.CreateAddress(strategy, coin.KeyPath, coin.ScriptPubKey); var paymentData = new BitcoinLikePaymentData(address, coin.Value, coin.OutPoint, transaction.Transaction.RBF); - + var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network).ConfigureAwait(false); alreadyAccounted.Add(coin.OutPoint); if (payment != null) { invoice = await ReceivedPayment(wallet, invoice, payment, strategy); - if(invoice == null) + if (invoice == null) continue; totalPayment++; } @@ -385,7 +351,7 @@ namespace BTCPayServer.Payments.Bitcoin invoice.SetPaymentMethod(paymentMethod); } wallet.InvalidateCache(strategy); - _Aggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment){Payment = payment}); + _Aggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment }); return invoice; } public async Task StopAsync(CancellationToken cancellationToken) From c8e1db210208a70062eaeff7159c957cbe14afe4 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Wed, 11 Mar 2020 21:11:07 +0900 Subject: [PATCH 264/810] Better event messages --- BTCPayServer/Events/NewOnChainTransactionEvent.cs | 6 ++++++ BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/BTCPayServer/Events/NewOnChainTransactionEvent.cs b/BTCPayServer/Events/NewOnChainTransactionEvent.cs index b994f6b49..d695fac15 100644 --- a/BTCPayServer/Events/NewOnChainTransactionEvent.cs +++ b/BTCPayServer/Events/NewOnChainTransactionEvent.cs @@ -6,5 +6,11 @@ namespace BTCPayServer.Events { public NewTransactionEvent NewTransactionEvent { get; set; } public string CryptoCode { get; set; } + + public override string ToString() + { + var state = NewTransactionEvent.BlockId == null ? "Unconfirmed" : "Confirmed"; + return $"{CryptoCode}: New transaction {NewTransactionEvent.TransactionData.TransactionHash} ({state})"; + } } } diff --git a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs index 7f0b82990..674f29810 100644 --- a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs +++ b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs @@ -346,7 +346,7 @@ namespace BTCPayServer.Payments.Bitcoin var address = await wallet.ReserveAddressAsync(strategy); btc.DepositAddress = address.Address.ToString(); await _InvoiceRepository.NewAddress(invoice.Id, btc, wallet.Network); - _Aggregator.Publish(new InvoiceNewAddressEvent(invoice.Id, address.ToString(), wallet.Network)); + _Aggregator.Publish(new InvoiceNewAddressEvent(invoice.Id, btc.DepositAddress, wallet.Network)); paymentMethod.SetPaymentMethodDetails(btc); invoice.SetPaymentMethod(paymentMethod); } From afd2c8e3d7bab81e49c50fe07e58accd92672d5c Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Wed, 11 Mar 2020 22:32:53 +0900 Subject: [PATCH 265/810] Bump nbx --- BTCPayServer.Tests/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index ebfb469ac..1018b60fe 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -76,7 +76,7 @@ services: - customer_lnd - merchant_lnd nbxplorer: - image: nicolasdorier/nbxplorer:2.1.13 + image: nicolasdorier/nbxplorer:2.1.14 restart: unless-stopped ports: - "32838:32838" From 245507f821fb65878784746a73bba5ca921857dd Mon Sep 17 00:00:00 2001 From: Kukks Date: Wed, 11 Mar 2020 16:51:33 +0100 Subject: [PATCH 266/810] fix e2e tests --- BTCPayServer.Tests/CheckoutUITests.cs | 3 ++- BTCPayServer.Tests/Extensions.cs | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/BTCPayServer.Tests/CheckoutUITests.cs b/BTCPayServer.Tests/CheckoutUITests.cs index 46531aea9..3ae9531a0 100644 --- a/BTCPayServer.Tests/CheckoutUITests.cs +++ b/BTCPayServer.Tests/CheckoutUITests.cs @@ -205,7 +205,8 @@ namespace BTCPayServer.Tests IWebElement closebutton = null; TestUtils.Eventually(() => { - var iframe = s.Driver.SwitchTo().Frame("btcpay"); + var frameElement = s.Driver.FindElement(By.Name("btcpay")); + var iframe = s.Driver.SwitchTo().Frame(frameElement); closebutton = iframe.FindElement(By.ClassName("close-action")); Assert.True(closebutton.Displayed); }); diff --git a/BTCPayServer.Tests/Extensions.cs b/BTCPayServer.Tests/Extensions.cs index 0f6d36b4e..22f3822eb 100644 --- a/BTCPayServer.Tests/Extensions.cs +++ b/BTCPayServer.Tests/Extensions.cs @@ -88,6 +88,10 @@ namespace BTCPayServer.Tests if (!webElement.Displayed) return; } + catch (NoSuchWindowException) + { + return; + } catch (NoSuchElementException) { return; From 233fa8a4a1ff3a6987ac6db28573573d5d373dd6 Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 2 Mar 2020 16:50:28 +0100 Subject: [PATCH 267/810] BTCPayServer.Client library + Revoke API Key --- .../BTCPayServer.Client.csproj | 7 ++ .../BTCPayServerClient.APIKeys.cs | 22 +++++ .../BTCPayServerClient.Authorization.cs | 24 +++++ BTCPayServer.Client/BTCPayServerClient.cs | 96 +++++++++++++++++++ BTCPayServer.Client/Models/ApiKeyData.cs | 10 ++ BTCPayServer.Client/Permissions.cs | 18 ++++ BTCPayServer.Tests/ApiKeysTests.cs | 71 +++----------- BTCPayServer.Tests/GreenfieldAPITests.cs | 23 +++-- BTCPayServer/BTCPayServer.csproj | 1 + .../Controllers/ManageController.APIKeys.cs | 15 +-- .../Controllers/RestApi/ApiKeys/ApiKeyData.cs | 23 ----- .../RestApi/ApiKeys/ApiKeysController.cs | 32 ++++++- .../APIKeys/APIKeyAuthorizationHandler.cs | 11 ++- .../Security/APIKeys/APIKeyConstants.cs | 17 +--- .../Security/APIKeys/APIKeyExtensions.cs | 5 +- BTCPayServer/Views/Manage/AddApiKey.cshtml | 15 +-- .../Views/Manage/AuthorizeAPIKey.cshtml | 17 ++-- btcpayserver.sln | 14 +++ 18 files changed, 285 insertions(+), 136 deletions(-) create mode 100644 BTCPayServer.Client/BTCPayServer.Client.csproj create mode 100644 BTCPayServer.Client/BTCPayServerClient.APIKeys.cs create mode 100644 BTCPayServer.Client/BTCPayServerClient.Authorization.cs create mode 100644 BTCPayServer.Client/BTCPayServerClient.cs create mode 100644 BTCPayServer.Client/Models/ApiKeyData.cs create mode 100644 BTCPayServer.Client/Permissions.cs delete mode 100644 BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeyData.cs diff --git a/BTCPayServer.Client/BTCPayServer.Client.csproj b/BTCPayServer.Client/BTCPayServer.Client.csproj new file mode 100644 index 000000000..8642d92f3 --- /dev/null +++ b/BTCPayServer.Client/BTCPayServer.Client.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp3.1 + + + diff --git a/BTCPayServer.Client/BTCPayServerClient.APIKeys.cs b/BTCPayServer.Client/BTCPayServerClient.APIKeys.cs new file mode 100644 index 000000000..dcced301b --- /dev/null +++ b/BTCPayServer.Client/BTCPayServerClient.APIKeys.cs @@ -0,0 +1,22 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Client.Models; + +namespace BTCPayServer.Client +{ + public partial class BTCPayServerClient + { + public virtual async Task GetCurrentAPIKeyInfo(CancellationToken token = default) + { + var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/api-keys/current"), token); + return await HandleResponse(response); + } + + public virtual async Task RevokeCurrentAPIKeyInfo(CancellationToken token = default) + { + var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/api-keys/current", null, HttpMethod.Delete), token); + HandleResponse(response); + } + } +} diff --git a/BTCPayServer.Client/BTCPayServerClient.Authorization.cs b/BTCPayServer.Client/BTCPayServerClient.Authorization.cs new file mode 100644 index 000000000..bfe3c891a --- /dev/null +++ b/BTCPayServer.Client/BTCPayServerClient.Authorization.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace BTCPayServer.Client +{ + public partial class BTCPayServerClient + { + + public static Uri GenerateAuthorizeUri(Uri btcpayHost, string[] permissions, bool strict = true, + bool selectiveStores = false) + { + var result = new UriBuilder(btcpayHost); + result.Path = "api-keys/authorize"; + + AppendPayloadToQuery(result, + new Dictionary() + { + {"strict", strict}, {"selectiveStores", selectiveStores}, {"permissions", permissions} + }); + + return result.Uri; + } + } +} diff --git a/BTCPayServer.Client/BTCPayServerClient.cs b/BTCPayServer.Client/BTCPayServerClient.cs new file mode 100644 index 000000000..53ad88d0c --- /dev/null +++ b/BTCPayServer.Client/BTCPayServerClient.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Threading.Tasks; + +namespace BTCPayServer.Client +{ + public partial class BTCPayServerClient + { + private readonly string _apiKey; + private readonly Uri _btcpayHost; + private readonly HttpClient _httpClient; + + private readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + public BTCPayServerClient(Uri btcpayHost, string APIKey, HttpClient httpClient = null) + { + _apiKey = APIKey; + _btcpayHost = btcpayHost; + _httpClient = httpClient ?? new HttpClient(); + } + + protected void HandleResponse(HttpResponseMessage message) + { + message.EnsureSuccessStatusCode(); + } + + protected async Task HandleResponse(HttpResponseMessage message) + { + HandleResponse(message); + return JsonSerializer.Deserialize(await message.Content.ReadAsStringAsync(), _serializerOptions); + } + + protected virtual HttpRequestMessage CreateHttpRequest(string path, + Dictionary queryPayload = null, + HttpMethod method = null) + { + UriBuilder uriBuilder = new UriBuilder(_btcpayHost) {Path = path}; + if (queryPayload != null && queryPayload.Any()) + { + AppendPayloadToQuery(uriBuilder, queryPayload); + } + + var httpRequest = new HttpRequestMessage(method ?? HttpMethod.Get, uriBuilder.Uri); + httpRequest.Headers.Authorization = new AuthenticationHeaderValue("token", _apiKey); + + + return httpRequest; + } + + protected virtual HttpRequestMessage CreateHttpRequest(string path, + Dictionary queryPayload = null, + T bodyPayload = default, HttpMethod method = null) + { + var request = CreateHttpRequest(path, queryPayload, method); + if (typeof(T).IsPrimitive || !EqualityComparer.Default.Equals(bodyPayload, default(T))) + { + request.Content = new StringContent(JsonSerializer.Serialize(bodyPayload, _serializerOptions)); + } + + return request; + } + + private static void AppendPayloadToQuery(UriBuilder uri, Dictionary payload) + { + if (uri.Query.Length > 1) + uri.Query += "&"; + foreach (KeyValuePair keyValuePair in payload) + { + UriBuilder uriBuilder = uri; + if (keyValuePair.Value.GetType().GetInterfaces().Contains((typeof(IEnumerable)))) + { + foreach (var item in (IEnumerable)keyValuePair.Value) + { + uriBuilder.Query = uriBuilder.Query + Uri.EscapeDataString(keyValuePair.Key) + "=" + + Uri.EscapeDataString(item.ToString()) + "&"; + } + } + else + { + uriBuilder.Query = uriBuilder.Query + Uri.EscapeDataString(keyValuePair.Key) + "=" + + Uri.EscapeDataString(keyValuePair.Value.ToString()) + "&"; + } + } + + uri.Query = uri.Query.Trim('&'); + } + } +} diff --git a/BTCPayServer.Client/Models/ApiKeyData.cs b/BTCPayServer.Client/Models/ApiKeyData.cs new file mode 100644 index 000000000..58872cb11 --- /dev/null +++ b/BTCPayServer.Client/Models/ApiKeyData.cs @@ -0,0 +1,10 @@ +namespace BTCPayServer.Client.Models +{ + public class ApiKeyData + { + public string ApiKey { get; set; } + public string Label { get; set; } + public string UserId { get; set; } + public string[] Permissions { get; set; } + } +} diff --git a/BTCPayServer.Client/Permissions.cs b/BTCPayServer.Client/Permissions.cs new file mode 100644 index 000000000..adb9ccfa5 --- /dev/null +++ b/BTCPayServer.Client/Permissions.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace BTCPayServer.Client +{ + public static class Permissions + { + public const string ServerManagement = nameof(ServerManagement); + public const string StoreManagement = nameof(StoreManagement); + public static string GetStorePermission(string storeId) => $"{nameof(StoreManagement)}:{storeId}"; + + public static IEnumerable ExtractStorePermissionsIds(IEnumerable permissions) => permissions + .Where(s => s.StartsWith($"{nameof(StoreManagement)}:", StringComparison.InvariantCulture)) + .Select(s => s.Split(":")[1]); + } +} diff --git a/BTCPayServer.Tests/ApiKeysTests.cs b/BTCPayServer.Tests/ApiKeysTests.cs index f27760243..0669dbcae 100644 --- a/BTCPayServer.Tests/ApiKeysTests.cs +++ b/BTCPayServer.Tests/ApiKeysTests.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; +using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Security.APIKeys; using BTCPayServer.Tests.Logging; @@ -67,8 +68,8 @@ namespace BTCPayServer.Tests var superApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; //this api key has access to everything - await TestApiAgainstAccessToken(superApiKey, tester, user, APIKeyConstants.Permissions.ServerManagement, - APIKeyConstants.Permissions.StoreManagement); + await TestApiAgainstAccessToken(superApiKey, tester, user, Permissions.ServerManagement, + Permissions.StoreManagement); s.Driver.FindElement(By.Id("AddApiKey")).Click(); @@ -76,7 +77,7 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("Generate")).Click(); var serverOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user, - APIKeyConstants.Permissions.ServerManagement); + Permissions.ServerManagement); s.Driver.FindElement(By.Id("AddApiKey")).Click(); @@ -84,7 +85,7 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("Generate")).Click(); var allStoreOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; await TestApiAgainstAccessToken(allStoreOnlyApiKey, tester, user, - APIKeyConstants.Permissions.StoreManagement); + Permissions.StoreManagement); s.Driver.FindElement(By.Id("AddApiKey")).Click(); s.Driver.FindElement(By.CssSelector("button[value=change-store-mode]")).Click(); @@ -96,7 +97,7 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("Generate")).Click(); var selectiveStoreApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; await TestApiAgainstAccessToken(selectiveStoreApiKey, tester, user, - APIKeyConstants.Permissions.GetStorePermission(storeId)); + Permissions.GetStorePermission(storeId)); s.Driver.FindElement(By.Id("AddApiKey")).Click(); s.Driver.FindElement(By.Id("Generate")).Click(); @@ -117,31 +118,8 @@ namespace BTCPayServer.Tests //permissions //strict //selectiveStores - UriBuilder authorize = new UriBuilder(tester.PayTester.ServerUri); - authorize.Path = "api-keys/authorize"; - - authorize.AppendPayloadToQuery(new Dictionary() - { - {"redirect", "https://local.local/callback"}, - {"applicationName", "kukksappname"}, - {"strict", true}, - {"selectiveStores", false}, - { - "permissions", - new[] - { - APIKeyConstants.Permissions.StoreManagement, - APIKeyConstants.Permissions.ServerManagement - } - }, - }); - var authUrl = authorize.ToString(); - var perms = new[] - { - APIKeyConstants.Permissions.StoreManagement, APIKeyConstants.Permissions.ServerManagement - }; - authUrl = authUrl.Replace("permissions=System.String%5B%5D", - string.Join("&", perms.Select(s1 => $"permissions={s1}"))); + var authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri, + new[] {Permissions.StoreManagement, Permissions.ServerManagement}).ToString(); s.Driver.Navigate().GoToUrl(authUrl); s.Driver.PageSource.Contains("kukksappname"); Assert.NotNull(s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("readonly")); @@ -159,28 +137,9 @@ namespace BTCPayServer.Tests await TestApiAgainstAccessToken(results.Single(pair => pair.Key == "key").Value, tester, user, (await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).GetPermissions()); - authorize = new UriBuilder(tester.PayTester.ServerUri); - authorize.Path = "api-keys/authorize"; - authorize.AppendPayloadToQuery(new Dictionary() - { - {"strict", false}, - {"selectiveStores", true}, - { - "permissions", - new[] - { - APIKeyConstants.Permissions.StoreManagement, - APIKeyConstants.Permissions.ServerManagement - } - } - }); - authUrl = authorize.ToString(); - perms = new[] - { - APIKeyConstants.Permissions.StoreManagement, APIKeyConstants.Permissions.ServerManagement - }; - authUrl = authUrl.Replace("permissions=System.String%5B%5D", - string.Join("&", perms.Select(s1 => $"permissions={s1}"))); + authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri, + new[] {Permissions.StoreManagement, Permissions.ServerManagement}).ToString(); + s.Driver.Navigate().GoToUrl(authUrl); Assert.DoesNotContain("kukksappname", s.Driver.PageSource); @@ -214,8 +173,8 @@ namespace BTCPayServer.Tests var secondUser = tester.NewAccount(); secondUser.GrantAccess(); - var selectiveStorePermissions = APIKeyConstants.Permissions.ExtractStorePermissionsIds(permissions); - if (permissions.Contains(APIKeyConstants.Permissions.StoreManagement) || selectiveStorePermissions.Any()) + var selectiveStorePermissions = Permissions.ExtractStorePermissionsIds(permissions); + if (permissions.Contains(Permissions.StoreManagement) || selectiveStorePermissions.Any()) { var resultStores = await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/stores", @@ -231,7 +190,7 @@ namespace BTCPayServer.Tests data => data.Id.Equals(selectiveStorePermission, StringComparison.InvariantCultureIgnoreCase)); } - if (permissions.Contains(APIKeyConstants.Permissions.StoreManagement)) + if (permissions.Contains(Permissions.StoreManagement)) { Assert.True(await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/stores/actions", @@ -272,7 +231,7 @@ namespace BTCPayServer.Tests tester.PayTester.HttpClient); }); - if (permissions.Contains(APIKeyConstants.Permissions.ServerManagement)) + if (permissions.Contains(Permissions.ServerManagement)) { Assert.True(await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/is-admin", diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index d2ba99676..497dc69b2 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -1,14 +1,10 @@ -using System; using System.Net.Http; -using System.Net.Http.Headers; using System.Threading.Tasks; +using BTCPayServer.Client; using BTCPayServer.Controllers; -using BTCPayServer.Controllers.RestApi.ApiKeys; -using BTCPayServer.Data; -using BTCPayServer.Security.APIKeys; using BTCPayServer.Tests.Logging; +using Microsoft.AspNet.SignalR.Client; using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json.Linq; using Xunit; using Xunit.Abstractions; @@ -37,17 +33,20 @@ namespace BTCPayServer.Tests user.GrantAccess(); await user.MakeAdmin(); string apiKey = await GenerateAPIKey(tester, user); - + var client = new BTCPayServerClient(tester.PayTester.ServerUri, apiKey); //Get current api key - var request = new HttpRequestMessage(HttpMethod.Get, "api/v1/api-keys/current"); - request.Headers.Authorization = new AuthenticationHeaderValue("token", apiKey); - var result = await tester.PayTester.HttpClient.SendAsync(request); - Assert.True(result.IsSuccessStatusCode); - var apiKeyData = JObject.Parse(await result.Content.ReadAsStringAsync()).ToObject(); + var apiKeyData = await client.GetCurrentAPIKeyInfo(); Assert.NotNull(apiKeyData); Assert.Equal(apiKey, apiKeyData.ApiKey); Assert.Equal(user.UserId, apiKeyData.UserId); Assert.Equal(2, apiKeyData.Permissions.Length); + + //revoke current api key + await client.RevokeCurrentAPIKeyInfo(); + await Assert.ThrowsAsync(async () => + { + await client.GetCurrentAPIKeyInfo(); + }); } } diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 68d803f81..9d69f7fff 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -124,6 +124,7 @@ + diff --git a/BTCPayServer/Controllers/ManageController.APIKeys.cs b/BTCPayServer/Controllers/ManageController.APIKeys.cs index b5abfbb0e..8dd638788 100644 --- a/BTCPayServer/Controllers/ManageController.APIKeys.cs +++ b/BTCPayServer/Controllers/ManageController.APIKeys.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Hosting.OpenApi; using BTCPayServer.Models; @@ -109,8 +110,8 @@ namespace BTCPayServer.Controllers var vm = await SetViewModelValues(new AuthorizeApiKeysViewModel() { Label = applicationName, - ServerManagementPermission = permissions.Contains(APIKeyConstants.Permissions.ServerManagement), - StoreManagementPermission = permissions.Contains(APIKeyConstants.Permissions.StoreManagement), + ServerManagementPermission = permissions.Contains(Permissions.ServerManagement), + StoreManagementPermission = permissions.Contains(Permissions.StoreManagement), PermissionsFormatted = permissions, ApplicationName = applicationName, SelectiveStores = selectiveStores, @@ -133,7 +134,7 @@ namespace BTCPayServer.Controllers } - if (viewModel.PermissionsFormatted.Contains(APIKeyConstants.Permissions.ServerManagement)) + if (viewModel.PermissionsFormatted.Contains(Permissions.ServerManagement)) { if (!viewModel.IsServerAdmin && viewModel.ServerManagementPermission) { @@ -147,7 +148,7 @@ namespace BTCPayServer.Controllers } } - if (viewModel.PermissionsFormatted.Contains(APIKeyConstants.Permissions.StoreManagement)) + if (viewModel.PermissionsFormatted.Contains(Permissions.StoreManagement)) { if (!viewModel.SelectiveStores && viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific) @@ -265,16 +266,16 @@ namespace BTCPayServer.Controllers if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific) { - permissions.AddRange(viewModel.SpecificStores.Select(APIKeyConstants.Permissions.GetStorePermission)); + permissions.AddRange(viewModel.SpecificStores.Select(Permissions.GetStorePermission)); } else if (viewModel.StoreManagementPermission) { - permissions.Add(APIKeyConstants.Permissions.StoreManagement); + permissions.Add(Permissions.StoreManagement); } if (viewModel.IsServerAdmin && viewModel.ServerManagementPermission) { - permissions.Add(APIKeyConstants.Permissions.ServerManagement); + permissions.Add(Permissions.ServerManagement); } return permissions; diff --git a/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeyData.cs b/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeyData.cs deleted file mode 100644 index b308976c3..000000000 --- a/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeyData.cs +++ /dev/null @@ -1,23 +0,0 @@ -using BTCPayServer.Data; - -namespace BTCPayServer.Controllers.RestApi.ApiKeys -{ - public class ApiKeyData - { - public string ApiKey { get; set; } - public string Label { get; set; } - public string UserId { get; set; } - public string[] Permissions { get; set; } - - public static ApiKeyData FromModel(APIKeyData data) - { - return new ApiKeyData() - { - Permissions = data.GetPermissions(), - ApiKey = data.Id, - UserId = data.UserId, - Label = data.Label - }; - } - } -} diff --git a/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs b/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs index ed45d77d6..486c048ff 100644 --- a/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs +++ b/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs @@ -1,9 +1,12 @@ using System.Threading.Tasks; +using BTCPayServer.Client.Models; +using BTCPayServer.Data; using BTCPayServer.Hosting.OpenApi; using BTCPayServer.Security; using BTCPayServer.Security.APIKeys; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; @@ -16,10 +19,12 @@ namespace BTCPayServer.Controllers.RestApi.ApiKeys public class ApiKeysController : ControllerBase { private readonly APIKeyRepository _apiKeyRepository; + private readonly UserManager _userManager; - public ApiKeysController(APIKeyRepository apiKeyRepository) + public ApiKeysController(APIKeyRepository apiKeyRepository, UserManager userManager) { _apiKeyRepository = apiKeyRepository; + _userManager = userManager; } [OpenApiOperation("Get current API Key information", "View information about the current API key")] @@ -31,7 +36,30 @@ namespace BTCPayServer.Controllers.RestApi.ApiKeys { ControllerContext.HttpContext.GetAPIKey(out var apiKey); var data = await _apiKeyRepository.GetKey(apiKey); - return Ok(ApiKeyData.FromModel(data)); + return Ok(FromModel(data)); + } + + [OpenApiOperation("Revoke the current API Key", "Revoke the current API key so that it cannot be used anymore")] + [SwaggerResponse(StatusCodes.Status200OK, typeof(ApiKeyData), + Description = "The key was revoked and is no longer usable")] + [HttpDelete("~/api/v1/api-keys/current")] + [HttpDelete("~/api/v1/users/me/api-keys/current")] + public async Task> RevokeKey() + { + ControllerContext.HttpContext.GetAPIKey(out var apiKey); + await _apiKeyRepository.Remove(apiKey, _userManager.GetUserId(User)); + return Ok(); + } + + private static ApiKeyData FromModel(APIKeyData data) + { + return new ApiKeyData() + { + Permissions = data.GetPermissions(), + ApiKey = data.Id, + UserId = data.UserId, + Label = data.Label + }; } } } diff --git a/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs b/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs index 0ed687374..f38d05ce2 100644 --- a/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs +++ b/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Services.Stores; using Microsoft.AspNetCore.Authorization; @@ -35,14 +36,14 @@ namespace BTCPayServer.Security.APIKeys { case Policies.CanListStoreSettings.Key: var selectiveStorePermissions = - APIKeyConstants.Permissions.ExtractStorePermissionsIds(context.GetPermissions()); - success = context.HasPermissions(APIKeyConstants.Permissions.StoreManagement) || + Permissions.ExtractStorePermissionsIds(context.GetPermissions()); + success = context.HasPermissions(Permissions.StoreManagement) || selectiveStorePermissions.Any(); break; case Policies.CanModifyStoreSettings.Key: string storeId = _HttpContext.GetImplicitStoreId(); - if (!context.HasPermissions(APIKeyConstants.Permissions.StoreManagement) && - !context.HasPermissions(APIKeyConstants.Permissions.GetStorePermission(storeId))) + if (!context.HasPermissions(Permissions.StoreManagement) && + !context.HasPermissions(Permissions.GetStorePermission(storeId))) break; if (storeId == null) @@ -63,7 +64,7 @@ namespace BTCPayServer.Security.APIKeys break; case Policies.CanModifyServerSettings.Key: - if (!context.HasPermissions(APIKeyConstants.Permissions.ServerManagement)) + if (!context.HasPermissions(Permissions.ServerManagement)) break; // For this authorization, we stil check in database because it is super sensitive. var user = await _userManager.GetUserAsync(context.User); diff --git a/BTCPayServer/Security/APIKeys/APIKeyConstants.cs b/BTCPayServer/Security/APIKeys/APIKeyConstants.cs index 427de68e5..0d1893f49 100644 --- a/BTCPayServer/Security/APIKeys/APIKeyConstants.cs +++ b/BTCPayServer/Security/APIKeys/APIKeyConstants.cs @@ -1,7 +1,4 @@ -using System; using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; namespace BTCPayServer.Security.APIKeys { @@ -16,21 +13,13 @@ namespace BTCPayServer.Security.APIKeys public static class Permissions { - public const string ServerManagement = nameof(ServerManagement); - public const string StoreManagement = nameof(StoreManagement); - public static readonly Dictionary PermissionDescriptions = new Dictionary() { - {StoreManagement, ("Manage your stores", "The app will be able to create, modify and delete all your stores.")}, - {$"{nameof(StoreManagement)}:", ("Manage selected stores", "The app will be able to modify and delete selected stores.")}, - {ServerManagement, ("Manage your server", "The app will have total control on your server")}, + {Client.Permissions.StoreManagement, ("Manage your stores", "The app will be able to create, modify and delete all your stores.")}, + {$"{nameof(Client.Permissions.StoreManagement)}:", ("Manage selected stores", "The app will be able to modify and delete selected stores.")}, + {Client.Permissions.ServerManagement, ("Manage your server", "The app will have total control on your server")}, }; - public static string GetStorePermission(string storeId) => $"{nameof(StoreManagement)}:{storeId}"; - - public static IEnumerable ExtractStorePermissionsIds(IEnumerable permissions) => permissions - .Where(s => s.StartsWith($"{nameof(StoreManagement)}:", StringComparison.InvariantCulture)) - .Select(s => s.Split(":")[1]); } } } diff --git a/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs b/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs index 17f696f0d..6f5429011 100644 --- a/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs +++ b/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; +using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Security.Bitpay; using BTCPayServer.Services.Stores; @@ -35,12 +36,12 @@ namespace BTCPayServer.Security.APIKeys claimsPrincipal.Claims.Where(claim => claim.Type == APIKeyConstants.ClaimTypes.Permissions) .Select(claim => claim.Value).ToList(); - if (permissions.Contains(APIKeyConstants.Permissions.StoreManagement)) + if (permissions.Contains(Permissions.StoreManagement)) { return storeRepository.GetStoresByUserId(userManager.GetUserId(claimsPrincipal)); } - var storeIds = APIKeyConstants.Permissions.ExtractStorePermissionsIds(permissions); + var storeIds = Permissions.ExtractStorePermissionsIds(permissions); return storeRepository.GetStoresByUserId(userManager.GetUserId(claimsPrincipal), storeIds); } diff --git a/BTCPayServer/Views/Manage/AddApiKey.cshtml b/BTCPayServer/Views/Manage/AddApiKey.cshtml index f5316c545..dd1fabada 100644 --- a/BTCPayServer/Views/Manage/AddApiKey.cshtml +++ b/BTCPayServer/Views/Manage/AddApiKey.cshtml @@ -1,6 +1,7 @@ +@using BTCPayServer.Client @using BTCPayServer.Controllers @using BTCPayServer.Security.APIKeys -@model BTCPayServer.Controllers.ManageController.AddApiKeyViewModel +@model BTCPayServer.Controllers.ManageController.AuthorizeApiKeysViewModel @{ ViewData.SetActivePageAndTitle(ManageNavPages.APIKeys, "Add API Key"); @@ -39,9 +40,9 @@ {
    - + -

    @GetDescription(APIKeyConstants.Permissions.ServerManagement).

    +

    @GetDescription(Permissions.ServerManagement).

    } @if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores) @@ -49,9 +50,9 @@
    @Html.CheckBoxFor(model => model.StoreManagementPermission, new Dictionary() {{"class", "form-check-inline"}}) - + -

    @GetDescription(APIKeyConstants.Permissions.StoreManagement).

    +

    @GetDescription(Permissions.StoreManagement).

    } @@ -59,8 +60,8 @@ {
  • -
    @GetTitle(APIKeyConstants.Permissions.StoreManagement + ":")
    -

    @GetDescription(APIKeyConstants.Permissions.StoreManagement + ":").

    +
    @GetTitle(Permissions.StoreManagement + ":")
    +

    @GetDescription(Permissions.StoreManagement + ":").

  • @if (!Model.Stores.Any()) diff --git a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml index 303e171aa..71310d7c4 100644 --- a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml +++ b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml @@ -1,3 +1,4 @@ +@using BTCPayServer.Client @using BTCPayServer.Controllers @using BTCPayServer.Security.APIKeys @model BTCPayServer.Controllers.ManageController.AuthorizeApiKeysViewModel @@ -48,11 +49,11 @@

    There are no associated permissions to the API key being requested here. The application cannot do anything with your BTCPay account other than validating your account exists.

    } - @if (Model.PermissionsFormatted.Contains(APIKeyConstants.Permissions.ServerManagement) && (Model.IsServerAdmin || Model.Strict)) + @if (Model.PermissionsFormatted.Contains(Permissions.ServerManagement) && (Model.IsServerAdmin || Model.Strict)) {
    - + @if (!Model.IsServerAdmin) { @@ -61,19 +62,19 @@ } -

    @GetDescription(APIKeyConstants.Permissions.ServerManagement).

    +

    @GetDescription(Permissions.ServerManagement).

    } - @if (Model.PermissionsFormatted.Contains(APIKeyConstants.Permissions.StoreManagement)) + @if (Model.PermissionsFormatted.Contains(Permissions.StoreManagement)) { @if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores) {
    - + -

    @GetDescription(APIKeyConstants.Permissions.StoreManagement).

    +

    @GetDescription(Permissions.StoreManagement).

    @if (Model.SelectiveStores) { @@ -84,8 +85,8 @@ {
  • -
    @GetTitle(APIKeyConstants.Permissions.StoreManagement + ":")
    -

    @GetDescription(APIKeyConstants.Permissions.StoreManagement + ":").

    +
    @GetTitle(Permissions.StoreManagement + ":")
    +

    @GetDescription(Permissions.StoreManagement + ":").

  • @if (!Model.Stores.Any()) diff --git a/btcpayserver.sln b/btcpayserver.sln index 72fc1a167..29f7e4822 100644 --- a/btcpayserver.sln +++ b/btcpayserver.sln @@ -25,6 +25,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer.Common", "BTCP EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer.Data", "BTCPayServer.Data\BTCPayServer.Data.csproj", "{4D7A865D-3945-4C70-9CC8-B09A274A697E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Client", "BTCPayServer.Client\BTCPayServer.Client.csproj", "{21A13304-7168-49A0-86C2-0A1A9453E9C7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -95,6 +97,18 @@ Global {4D7A865D-3945-4C70-9CC8-B09A274A697E}.Release|x64.Build.0 = Release|Any CPU {4D7A865D-3945-4C70-9CC8-B09A274A697E}.Release|x86.ActiveCfg = Release|Any CPU {4D7A865D-3945-4C70-9CC8-B09A274A697E}.Release|x86.Build.0 = Release|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Debug|x64.ActiveCfg = Debug|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Debug|x64.Build.0 = Debug|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Debug|x86.ActiveCfg = Debug|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Debug|x86.Build.0 = Debug|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Release|Any CPU.Build.0 = Release|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Release|x64.ActiveCfg = Release|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Release|x64.Build.0 = Release|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Release|x86.ActiveCfg = Release|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 786be9d1f506fc275e0bf1c8eb65f51d106dff7c Mon Sep 17 00:00:00 2001 From: Kukks Date: Sun, 8 Mar 2020 15:44:37 +0100 Subject: [PATCH 268/810] fix tests --- BTCPayServer.Tests/ApiKeysTests.cs | 18 ++++++++-------- BTCPayServer/Views/Manage/AddApiKey.cshtml | 2 +- .../Views/Manage/AuthorizeAPIKey.cshtml | 21 +++++++++++++++++-- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/BTCPayServer.Tests/ApiKeysTests.cs b/BTCPayServer.Tests/ApiKeysTests.cs index 0669dbcae..dedad633b 100644 --- a/BTCPayServer.Tests/ApiKeysTests.cs +++ b/BTCPayServer.Tests/ApiKeysTests.cs @@ -122,10 +122,10 @@ namespace BTCPayServer.Tests new[] {Permissions.StoreManagement, Permissions.ServerManagement}).ToString(); s.Driver.Navigate().GoToUrl(authUrl); s.Driver.PageSource.Contains("kukksappname"); - Assert.NotNull(s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("readonly")); - Assert.True(s.Driver.FindElement(By.Id("StoreManagementPermission")).Selected); - Assert.NotNull(s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("readonly")); - Assert.True(s.Driver.FindElement(By.Id("ServerManagementPermission")).Selected); + Assert.Equal("hidden", s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("type").ToLowerInvariant()); + Assert.Equal("true", s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("value").ToLowerInvariant()); + Assert.Equal("hidden", s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("type").ToLowerInvariant()); + Assert.Equal("true",s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("value").ToLowerInvariant()); Assert.DoesNotContain("change-store-mode", s.Driver.PageSource); s.Driver.FindElement(By.Id("consent-yes")).Click(); var url = s.Driver.Url; @@ -138,15 +138,15 @@ namespace BTCPayServer.Tests (await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).GetPermissions()); authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri, - new[] {Permissions.StoreManagement, Permissions.ServerManagement}).ToString(); + new[] {Permissions.StoreManagement, Permissions.ServerManagement}, false, true).ToString(); s.Driver.Navigate().GoToUrl(authUrl); Assert.DoesNotContain("kukksappname", s.Driver.PageSource); - Assert.Null(s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("readonly")); - Assert.True(s.Driver.FindElement(By.Id("StoreManagementPermission")).Selected); - Assert.Null(s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("readonly")); - Assert.True(s.Driver.FindElement(By.Id("ServerManagementPermission")).Selected); + Assert.Equal("checkbox", s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("type").ToLowerInvariant()); + Assert.Equal("true", s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("value").ToLowerInvariant()); + Assert.Equal("checkbox", s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("type").ToLowerInvariant()); + Assert.Equal("true",s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("value").ToLowerInvariant()); s.SetCheckbox(s, "ServerManagementPermission", false); Assert.Contains("change-store-mode", s.Driver.PageSource); diff --git a/BTCPayServer/Views/Manage/AddApiKey.cshtml b/BTCPayServer/Views/Manage/AddApiKey.cshtml index dd1fabada..734b1c333 100644 --- a/BTCPayServer/Views/Manage/AddApiKey.cshtml +++ b/BTCPayServer/Views/Manage/AddApiKey.cshtml @@ -1,7 +1,7 @@ @using BTCPayServer.Client @using BTCPayServer.Controllers @using BTCPayServer.Security.APIKeys -@model BTCPayServer.Controllers.ManageController.AuthorizeApiKeysViewModel +@model ManageController.AddApiKeyViewModel @{ ViewData.SetActivePageAndTitle(ManageNavPages.APIKeys, "Add API Key"); diff --git a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml index 71310d7c4..aef2734dc 100644 --- a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml +++ b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml @@ -52,7 +52,16 @@ @if (Model.PermissionsFormatted.Contains(Permissions.ServerManagement) && (Model.IsServerAdmin || Model.Strict)) {
    - + @if (Model.Strict || !Model.IsServerAdmin) + { + + + } + else + { + + } + @if (!Model.IsServerAdmin) { @@ -71,7 +80,15 @@ @if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores) {
    - + @if (Model.Strict) + { + + + } + else + { + + }

    @GetDescription(Permissions.StoreManagement).

    From 2002c6750b060a7e558587803b165a5e4c3830bc Mon Sep 17 00:00:00 2001 From: Kukks Date: Wed, 11 Mar 2020 15:46:21 +0100 Subject: [PATCH 269/810] target netstandard2.1 for Client --- BTCPayServer.Client/BTCPayServer.Client.csproj | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/BTCPayServer.Client/BTCPayServer.Client.csproj b/BTCPayServer.Client/BTCPayServer.Client.csproj index 8642d92f3..7522bd9e2 100644 --- a/BTCPayServer.Client/BTCPayServer.Client.csproj +++ b/BTCPayServer.Client/BTCPayServer.Client.csproj @@ -1,7 +1,11 @@ - netcoreapp3.1 + netstandard2.1 + + + + From 70b172addca7f76296a7944e8094e3bcacd5ed30 Mon Sep 17 00:00:00 2001 From: Kukks Date: Wed, 11 Mar 2020 18:05:40 +0100 Subject: [PATCH 270/810] Make api docs only available after login --- BTCPayServer/Hosting/BTCPayServerServices.cs | 1 - .../Hosting/OpenApi/OpenApiExtensions.cs | 23 +++++++++++++++++-- BTCPayServer/Hosting/Startup.cs | 2 ++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index d455efd65..602bd5343 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -292,7 +292,6 @@ namespace BTCPayServer.Hosting public static IApplicationBuilder UsePayServer(this IApplicationBuilder app) { app.UseMiddleware(); - app.UseBTCPayOpenApi(); return app; } public static IApplicationBuilder UseHeadersOverride(this IApplicationBuilder app) diff --git a/BTCPayServer/Hosting/OpenApi/OpenApiExtensions.cs b/BTCPayServer/Hosting/OpenApi/OpenApiExtensions.cs index 60c05330d..ece7bb57d 100644 --- a/BTCPayServer/Hosting/OpenApi/OpenApiExtensions.cs +++ b/BTCPayServer/Hosting/OpenApi/OpenApiExtensions.cs @@ -1,15 +1,20 @@ using System; using System.Collections.Generic; using System.Linq; +using BTCPayServer.Configuration; +using BTCPayServer.Data; using BTCPayServer.Payments; using BTCPayServer.Security; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using NJsonSchema; using NJsonSchema.Generation.TypeMappers; using NSwag; using NSwag.Generation.Processors.Security; +using Org.BouncyCastle.Asn1.Ocsp; namespace BTCPayServer.Hosting.OpenApi { @@ -17,7 +22,6 @@ namespace BTCPayServer.Hosting.OpenApi { public static IServiceCollection AddBTCPayOpenApi(this IServiceCollection serviceCollection) { - return serviceCollection.AddOpenApiDocument(config => { config.PostProcess = document => @@ -67,8 +71,23 @@ namespace BTCPayServer.Hosting.OpenApi public static IApplicationBuilder UseBTCPayOpenApi(this IApplicationBuilder builder) { + var roothPath = builder.ApplicationServices.GetService().RootPath; + var matched = new PathString($"{roothPath}docs"); return builder.UseOpenApi() - .UseReDoc(settings => settings.Path = "/docs"); + .Use(async (context, next) => + { + if (context.Request.Path.StartsWithSegments(matched, StringComparison.InvariantCultureIgnoreCase) && !context.User.Claims.Any()) + { + context.Response.Redirect( $"{context.Request.GetRelativePath(roothPath)}account/login?returnUrl={context.Request.Path}"); + return; + } + + await next.Invoke(); + }) + .UseReDoc(settings => + { + settings.Path = "/docs"; + }); } diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index 86eadaa71..61f0cdf06 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -19,6 +19,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using BTCPayServer.Security; using Microsoft.AspNetCore.Server.Kestrel.Core; using System.Net; +using BTCPayServer.Hosting.OpenApi; using BTCPayServer.PaymentRequest; using BTCPayServer.Services.Apps; using BTCPayServer.Storage; @@ -183,6 +184,7 @@ namespace BTCPayServer.Hosting app.UseProviderStorage(options); app.UseAuthentication(); app.UseAuthorization(); + app.UseBTCPayOpenApi(); app.UseSession(); app.UseWebSockets(); From 8173296c965c11687439273d283882b86fa458e6 Mon Sep 17 00:00:00 2001 From: Kukks Date: Thu, 12 Mar 2020 14:59:24 +0100 Subject: [PATCH 271/810] Greenfield API: Get current User Builds on #1368 This PR adds a new endpoint: Get current user.. It only returns the current user's id and email for now( let's extend later) It also adds a new permission: `ProfileManagement` which is needed for this endpoint (and for update endpoints later) --- .../BTCPayServerClient.Users.cs | 15 ++++++ .../Models/ApplicationUserData.cs | 8 ++++ BTCPayServer.Client/Permissions.cs | 11 +++++ BTCPayServer.Tests/ApiKeysTests.cs | 1 - BTCPayServer.Tests/GreenfieldAPITests.cs | 38 +++++++++++++-- .../Controllers/ManageController.APIKeys.cs | 25 ++++++++-- .../RestApi/Users/UsersController.cs | 47 +++++++++++++++++++ .../APIKeys/APIKeyAuthorizationHandler.cs | 3 ++ .../Security/APIKeys/APIKeyConstants.cs | 1 + BTCPayServer/Security/Policies.cs | 12 +++-- BTCPayServer/Views/Manage/AddApiKey.cshtml | 11 +++++ .../Views/Manage/AuthorizeAPIKey.cshtml | 19 +++++++- 12 files changed, 174 insertions(+), 17 deletions(-) create mode 100644 BTCPayServer.Client/BTCPayServerClient.Users.cs create mode 100644 BTCPayServer.Client/Models/ApplicationUserData.cs create mode 100644 BTCPayServer/Controllers/RestApi/Users/UsersController.cs diff --git a/BTCPayServer.Client/BTCPayServerClient.Users.cs b/BTCPayServer.Client/BTCPayServerClient.Users.cs new file mode 100644 index 000000000..d320c3792 --- /dev/null +++ b/BTCPayServer.Client/BTCPayServerClient.Users.cs @@ -0,0 +1,15 @@ +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Client.Models; + +namespace BTCPayServer.Client +{ + public partial class BTCPayServerClient + { + public virtual async Task GetCurrentUser(CancellationToken token = default) + { + var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/users/me"), token); + return await HandleResponse(response); + } + } +} diff --git a/BTCPayServer.Client/Models/ApplicationUserData.cs b/BTCPayServer.Client/Models/ApplicationUserData.cs new file mode 100644 index 000000000..6965271f8 --- /dev/null +++ b/BTCPayServer.Client/Models/ApplicationUserData.cs @@ -0,0 +1,8 @@ +namespace BTCPayServer.Client.Models +{ + public class ApplicationUserData + { + public string Id { get; set; } + public string Email { get; set; } + } +} diff --git a/BTCPayServer.Client/Permissions.cs b/BTCPayServer.Client/Permissions.cs index adb9ccfa5..ef26d83f5 100644 --- a/BTCPayServer.Client/Permissions.cs +++ b/BTCPayServer.Client/Permissions.cs @@ -9,6 +9,17 @@ namespace BTCPayServer.Client { public const string ServerManagement = nameof(ServerManagement); public const string StoreManagement = nameof(StoreManagement); + public const string ProfileManagement = nameof(ProfileManagement); + + public static string[] GetAllPermissionKeys() + { + return new[] + { + ServerManagement, + StoreManagement, + ProfileManagement + }; + } public static string GetStorePermission(string storeId) => $"{nameof(StoreManagement)}:{storeId}"; public static IEnumerable ExtractStorePermissionsIds(IEnumerable permissions) => permissions diff --git a/BTCPayServer.Tests/ApiKeysTests.cs b/BTCPayServer.Tests/ApiKeysTests.cs index dedad633b..1630863dd 100644 --- a/BTCPayServer.Tests/ApiKeysTests.cs +++ b/BTCPayServer.Tests/ApiKeysTests.cs @@ -9,7 +9,6 @@ using BTCPayServer.Data; using BTCPayServer.Security.APIKeys; using BTCPayServer.Tests.Logging; using BTCPayServer.Views.Manage; -using ExchangeSharp; using Newtonsoft.Json; using OpenQA.Selenium; using Xunit; diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 497dc69b2..9841a004e 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Net.Http; using System.Threading.Tasks; using BTCPayServer.Client; @@ -22,7 +23,7 @@ namespace BTCPayServer.Tests Logs.LogProvider = new XUnitLogProvider(helper); } - [Fact] + [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task ApiKeysControllerTests() { @@ -32,7 +33,7 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); user.GrantAccess(); await user.MakeAdmin(); - string apiKey = await GenerateAPIKey(tester, user); + string apiKey = await GenerateAPIKey(tester, user, Permissions.ServerManagement, Permissions.StoreManagement); var client = new BTCPayServerClient(tester.PayTester.ServerUri, apiKey); //Get current api key var apiKeyData = await client.GetCurrentAPIKeyInfo(); @@ -49,15 +50,42 @@ namespace BTCPayServer.Tests }); } } + + [Fact(Timeout = TestTimeout)] + [Trait("Integration", "Integration")] + public async Task UsersControllerTests() + { + using (var tester = ServerTester.Create()) + { + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + await user.MakeAdmin(); + string apiKeyProfile = await GenerateAPIKey(tester, user, Permissions.ProfileManagement); + string apiKeyInsufficient = await GenerateAPIKey(tester, user, Permissions.StoreManagement); + var clientProfile = new BTCPayServerClient(tester.PayTester.ServerUri, apiKeyProfile); + var clientInsufficient= new BTCPayServerClient(tester.PayTester.ServerUri, apiKeyInsufficient); + + var apiKeyProfileUserData = await clientProfile.GetCurrentUser(); + Assert.NotNull(apiKeyProfileUserData); + Assert.Equal(apiKeyProfileUserData.Id, user.UserId); + Assert.Equal(apiKeyProfileUserData.Email, user.RegisterDetails.Email); - private static async Task GenerateAPIKey(ServerTester tester, TestAccount user) + await Assert.ThrowsAsync(async () => await clientInsufficient.GetCurrentUser()); + } + } + + private static async Task GenerateAPIKey(ServerTester tester, TestAccount user, params string[] permissions) { var manageController = tester.PayTester.GetController(user.UserId, user.StoreId, user.IsAdmin); var x = Assert.IsType(await manageController.AddApiKey( new ManageController.AddApiKeyViewModel() { - ServerManagementPermission = true, - StoreManagementPermission = true, + PermissionValues = permissions.Select(s => new ManageController.AddApiKeyViewModel.PermissionValueItem() + { + Permission = s, + Value = true + }).ToList(), StoreMode = ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores })); var statusMessage = manageController.TempData.GetStatusMessageModel(); diff --git a/BTCPayServer/Controllers/ManageController.APIKeys.cs b/BTCPayServer/Controllers/ManageController.APIKeys.cs index 8dd638788..736fc2165 100644 --- a/BTCPayServer/Controllers/ManageController.APIKeys.cs +++ b/BTCPayServer/Controllers/ManageController.APIKeys.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc; using NBitcoin; using NBitcoin.DataEncoders; using NSwag.Annotations; +using YamlDotNet.Core.Tokens; namespace BTCPayServer.Controllers { @@ -30,8 +31,6 @@ namespace BTCPayServer.Controllers }) }); } - - [HttpGet("api-keys/{id}/delete")] public async Task RemoveAPIKey(string id) @@ -113,6 +112,10 @@ namespace BTCPayServer.Controllers ServerManagementPermission = permissions.Contains(Permissions.ServerManagement), StoreManagementPermission = permissions.Contains(Permissions.StoreManagement), PermissionsFormatted = permissions, + PermissionValues = permissions.Where(s => + !s.Contains(Permissions.StoreManagement, StringComparison.InvariantCultureIgnoreCase) && + s != Permissions.ServerManagement) + .Select(s => new AddApiKeyViewModel.PermissionValueItem() {Permission = s, Value = true}).ToList(), ApplicationName = applicationName, SelectiveStores = selectiveStores, Strict = strict, @@ -262,7 +265,7 @@ namespace BTCPayServer.Controllers private IEnumerable GetPermissionsFromViewModel(AddApiKeyViewModel viewModel) { - var permissions = new List(); + var permissions = viewModel.PermissionValues.Where(tuple => tuple.Value).Select(tuple => tuple.Permission).ToList(); if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific) { @@ -278,13 +281,18 @@ namespace BTCPayServer.Controllers permissions.Add(Permissions.ServerManagement); } - return permissions; + return permissions.Distinct(); } private async Task SetViewModelValues(T viewModel) where T : AddApiKeyViewModel { viewModel.Stores = await _StoreRepository.GetStoresByUserId(_userManager.GetUserId(User)); - viewModel.IsServerAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings.Key)).Succeeded; + viewModel.IsServerAdmin = + (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings.Key)).Succeeded; + viewModel.PermissionValues ??= Permissions.GetAllPermissionKeys().Where(s => + !s.Contains(Permissions.StoreManagement, StringComparison.InvariantCultureIgnoreCase) && + s != Permissions.ServerManagement) + .Select(s => new AddApiKeyViewModel.PermissionValueItem() {Permission = s, Value = true}).ToList(); return viewModel; } @@ -298,12 +306,19 @@ namespace BTCPayServer.Controllers public bool ServerManagementPermission { get; set; } public bool StoreManagementPermission { get; set; } public string Command { get; set; } + public List PermissionValues { get; set; } public enum ApiKeyStoreMode { AllStores, Specific } + + public class PermissionValueItem + { + public string Permission { get; set; } + public bool Value { get; set; } + } } public class AuthorizeApiKeysViewModel : AddApiKeyViewModel diff --git a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs new file mode 100644 index 000000000..f0bce9ec3 --- /dev/null +++ b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using BTCPayServer.Client.Models; +using BTCPayServer.Data; +using BTCPayServer.Hosting.OpenApi; +using BTCPayServer.Security; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; + +namespace BTCPayServer.Controllers.RestApi.Users +{ + [ApiController] + [IncludeInOpenApiDocs] + [OpenApiTags("Users")] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + public class UsersController : ControllerBase + { + private readonly UserManager _userManager; + + public UsersController(UserManager userManager) + { + _userManager = userManager; + } + + [OpenApiOperation("Get current user information", "View information about the current user")] + [SwaggerResponse(StatusCodes.Status200OK, typeof(ApiKeyData), + Description = "Information about the current user")] + [Authorize(Policy = Policies.CanModifyProfile.Key, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [HttpGet("~/api/v1/users/me")] + public async Task> GetCurrentUser() + { + var user = await _userManager.GetUserAsync(User); + return FromModel(user); + } + + private static ApplicationUserData FromModel(ApplicationUser data) + { + return new ApplicationUserData() + { + Id = data.Id, + Email = data.Email + }; + } + } +} diff --git a/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs b/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs index f38d05ce2..fe2f5bfc2 100644 --- a/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs +++ b/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs @@ -34,6 +34,9 @@ namespace BTCPayServer.Security.APIKeys bool success = false; switch (requirement.Policy) { + case Policies.CanModifyProfile.Key: + success = context.HasPermissions(Permissions.ProfileManagement); + break; case Policies.CanListStoreSettings.Key: var selectiveStorePermissions = Permissions.ExtractStorePermissionsIds(context.GetPermissions()); diff --git a/BTCPayServer/Security/APIKeys/APIKeyConstants.cs b/BTCPayServer/Security/APIKeys/APIKeyConstants.cs index 0d1893f49..f56598c9c 100644 --- a/BTCPayServer/Security/APIKeys/APIKeyConstants.cs +++ b/BTCPayServer/Security/APIKeys/APIKeyConstants.cs @@ -18,6 +18,7 @@ namespace BTCPayServer.Security.APIKeys {Client.Permissions.StoreManagement, ("Manage your stores", "The app will be able to create, modify and delete all your stores.")}, {$"{nameof(Client.Permissions.StoreManagement)}:", ("Manage selected stores", "The app will be able to modify and delete selected stores.")}, {Client.Permissions.ServerManagement, ("Manage your server", "The app will have total control on your server")}, + {Client.Permissions.ProfileManagement, ("Manage your profile", "The app will be able to view and modify your user profile.")}, }; } diff --git a/BTCPayServer/Security/Policies.cs b/BTCPayServer/Security/Policies.cs index e21c10c95..9ba065d07 100644 --- a/BTCPayServer/Security/Policies.cs +++ b/BTCPayServer/Security/Policies.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization; namespace BTCPayServer.Security { @@ -15,6 +11,8 @@ namespace BTCPayServer.Security options.AddPolicy(CanCreateInvoice.Key); options.AddPolicy(CanGetRates.Key); options.AddPolicy(CanModifyServerSettings.Key); + options.AddPolicy(CanModifyServerSettings.Key); + options.AddPolicy(CanModifyProfile.Key); return options; } @@ -27,6 +25,10 @@ namespace BTCPayServer.Security { public const string Key = "btcpay.store.canmodifyserversettings"; } + public class CanModifyProfile + { + public const string Key = "btcpay.store.canmodifyprofile"; + } public class CanModifyStoreSettings { public const string Key = "btcpay.store.canmodifystoresettings"; diff --git a/BTCPayServer/Views/Manage/AddApiKey.cshtml b/BTCPayServer/Views/Manage/AddApiKey.cshtml index 734b1c333..13791963a 100644 --- a/BTCPayServer/Views/Manage/AddApiKey.cshtml +++ b/BTCPayServer/Views/Manage/AddApiKey.cshtml @@ -45,6 +45,17 @@

    @GetDescription(Permissions.ServerManagement).

    } + + @for (int i = 0; i < Model.PermissionValues.Count; i++) + { +
    + + + + +

    @GetDescription(Model.PermissionValues[i].Permission).

    +
    + } @if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores) {
    diff --git a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml index aef2734dc..8ba022d87 100644 --- a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml +++ b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml @@ -74,7 +74,24 @@

    @GetDescription(Permissions.ServerManagement).

    } - + @for (int i = 0; i < Model.PermissionValues.Count; i++) + { +
    + + @if (Model.Strict || !Model.IsServerAdmin) + { + + + } + else + { + + } + + +

    @GetDescription(Model.PermissionValues[i].Permission).

    +
    + } @if (Model.PermissionsFormatted.Contains(Permissions.StoreManagement)) { @if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores) From 0c065df4bd8b79835d964796ef0b8e9967cd1b23 Mon Sep 17 00:00:00 2001 From: Kukks Date: Thu, 12 Mar 2020 18:43:57 +0100 Subject: [PATCH 272/810] Greenfield API: God Mode When the `ServerManagement` permission is granted, you should be able to do everything in the system. Maybe I should rename it to GodMode as a permission to not have any confusion with managing server settings (currently `ServerManagement`)? --- BTCPayServer.Tests/GreenfieldAPITests.cs | 3 +++ .../APIKeys/APIKeyAuthorizationHandler.cs | 27 ++++++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 9841a004e..937d1d5ae 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -62,8 +62,10 @@ namespace BTCPayServer.Tests user.GrantAccess(); await user.MakeAdmin(); string apiKeyProfile = await GenerateAPIKey(tester, user, Permissions.ProfileManagement); + string apiKeyServer = await GenerateAPIKey(tester, user, Permissions.ServerManagement); string apiKeyInsufficient = await GenerateAPIKey(tester, user, Permissions.StoreManagement); var clientProfile = new BTCPayServerClient(tester.PayTester.ServerUri, apiKeyProfile); + var clientServer = new BTCPayServerClient(tester.PayTester.ServerUri, apiKeyServer); var clientInsufficient= new BTCPayServerClient(tester.PayTester.ServerUri, apiKeyInsufficient); var apiKeyProfileUserData = await clientProfile.GetCurrentUser(); @@ -72,6 +74,7 @@ namespace BTCPayServer.Tests Assert.Equal(apiKeyProfileUserData.Email, user.RegisterDetails.Email); await Assert.ThrowsAsync(async () => await clientInsufficient.GetCurrentUser()); + await clientServer.GetCurrentUser(); } } diff --git a/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs b/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs index fe2f5bfc2..71c27e698 100644 --- a/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs +++ b/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs @@ -1,4 +1,5 @@ using System.Linq; +using System.Security.Claims; using System.Threading.Tasks; using BTCPayServer.Client; using BTCPayServer.Data; @@ -69,20 +70,32 @@ namespace BTCPayServer.Security.APIKeys case Policies.CanModifyServerSettings.Key: if (!context.HasPermissions(Permissions.ServerManagement)) break; - // For this authorization, we stil check in database because it is super sensitive. - var user = await _userManager.GetUserAsync(context.User); - if (user == null) - break; - if (!await _userManager.IsInRoleAsync(user, Roles.ServerAdmin)) - break; - success = true; + // For this authorization, we still check in database because it is super sensitive. + success = await IsUserAdmin(context.User); break; } + //if you do not have the specific permissions, BUT you have server management, we enable god mode + if (!success && context.HasPermissions(Permissions.ServerManagement) && + requirement.Policy != Policies.CanModifyServerSettings.Key) + { + success = await IsUserAdmin(context.User); + } + if (success) { context.Succeed(requirement); } } + + private async Task IsUserAdmin(ClaimsPrincipal contextUser) + { + var user = await _userManager.GetUserAsync(contextUser); + if (user == null) + return false; + if (!await _userManager.IsInRoleAsync(user, Roles.ServerAdmin)) + return false; + return true; + } } } From cc0202ecb34e2df5c43ae49a34dae1bdc9edc950 Mon Sep 17 00:00:00 2001 From: Kukks Date: Fri, 13 Mar 2020 07:58:09 +0100 Subject: [PATCH 273/810] fix test --- BTCPayServer.Tests/ApiKeysTests.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/BTCPayServer.Tests/ApiKeysTests.cs b/BTCPayServer.Tests/ApiKeysTests.cs index 1630863dd..e5318bd7c 100644 --- a/BTCPayServer.Tests/ApiKeysTests.cs +++ b/BTCPayServer.Tests/ApiKeysTests.cs @@ -214,8 +214,9 @@ namespace BTCPayServer.Tests Assert.DoesNotContain(resultStores, data => data.Id.Equals(secondUser.StoreId, StringComparison.InvariantCultureIgnoreCase)); } - else + else if(!permissions.Contains(Permissions.ServerManagement)) { + await Assert.ThrowsAnyAsync(async () => { await TestApiAgainstAccessToken(accessToken, @@ -224,11 +225,14 @@ namespace BTCPayServer.Tests }); } - await Assert.ThrowsAnyAsync(async () => + if (!permissions.Contains(Permissions.ServerManagement)) { - await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/stores/{secondUser.StoreId}/can-edit", - tester.PayTester.HttpClient); - }); + await Assert.ThrowsAnyAsync(async () => + { + await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/stores/{secondUser.StoreId}/can-edit", + tester.PayTester.HttpClient); + }); + } if (permissions.Contains(Permissions.ServerManagement)) { From ff2ea5815c18ca8e2eac3a283a9e7b0a506cfeb8 Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 16 Mar 2020 08:13:44 +0100 Subject: [PATCH 274/810] add else tests --- BTCPayServer.Tests/ApiKeysTests.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/BTCPayServer.Tests/ApiKeysTests.cs b/BTCPayServer.Tests/ApiKeysTests.cs index e5318bd7c..419fb2a60 100644 --- a/BTCPayServer.Tests/ApiKeysTests.cs +++ b/BTCPayServer.Tests/ApiKeysTests.cs @@ -224,6 +224,12 @@ namespace BTCPayServer.Tests tester.PayTester.HttpClient); }); } + else + { + await TestApiAgainstAccessToken(accessToken, + $"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit", + tester.PayTester.HttpClient); + } if (!permissions.Contains(Permissions.ServerManagement)) { @@ -233,6 +239,11 @@ namespace BTCPayServer.Tests tester.PayTester.HttpClient); }); } + else + { + await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/stores/{secondUser.StoreId}/can-edit", + tester.PayTester.HttpClient); + } if (permissions.Contains(Permissions.ServerManagement)) { @@ -240,6 +251,15 @@ namespace BTCPayServer.Tests $"{TestApiPath}/me/is-admin", tester.PayTester.HttpClient)); } + else + { + await Assert.ThrowsAnyAsync(async () => + { + await TestApiAgainstAccessToken(accessToken, + $"{TestApiPath}/me/is-admin", + tester.PayTester.HttpClient); + }); + } } public async Task TestApiAgainstAccessToken(string apikey, string url, HttpClient client) From 348934488de8e35518382910f3db51342072af8c Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Mon, 16 Mar 2020 16:36:55 +0900 Subject: [PATCH 275/810] Refactor tests for greenfield --- BTCPayServer.Client/BTCPayServerClient.cs | 2 ++ BTCPayServer.Tests/GreenfieldAPITests.cs | 37 +++++------------------ BTCPayServer.Tests/TestAccount.cs | 21 +++++++++++++ 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/BTCPayServer.Client/BTCPayServerClient.cs b/BTCPayServer.Client/BTCPayServerClient.cs index 53ad88d0c..a8f97bd30 100644 --- a/BTCPayServer.Client/BTCPayServerClient.cs +++ b/BTCPayServer.Client/BTCPayServerClient.cs @@ -15,6 +15,8 @@ namespace BTCPayServer.Client private readonly Uri _btcpayHost; private readonly HttpClient _httpClient; + public string APIKey => _apiKey; + private readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 937d1d5ae..890f1e902 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -33,12 +33,11 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); user.GrantAccess(); await user.MakeAdmin(); - string apiKey = await GenerateAPIKey(tester, user, Permissions.ServerManagement, Permissions.StoreManagement); - var client = new BTCPayServerClient(tester.PayTester.ServerUri, apiKey); + var client = await user.CreateClient(Permissions.ServerManagement, Permissions.StoreManagement); //Get current api key var apiKeyData = await client.GetCurrentAPIKeyInfo(); Assert.NotNull(apiKeyData); - Assert.Equal(apiKey, apiKeyData.ApiKey); + Assert.Equal(client.APIKey, apiKeyData.ApiKey); Assert.Equal(user.UserId, apiKeyData.UserId); Assert.Equal(2, apiKeyData.Permissions.Length); @@ -61,13 +60,11 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); user.GrantAccess(); await user.MakeAdmin(); - string apiKeyProfile = await GenerateAPIKey(tester, user, Permissions.ProfileManagement); - string apiKeyServer = await GenerateAPIKey(tester, user, Permissions.ServerManagement); - string apiKeyInsufficient = await GenerateAPIKey(tester, user, Permissions.StoreManagement); - var clientProfile = new BTCPayServerClient(tester.PayTester.ServerUri, apiKeyProfile); - var clientServer = new BTCPayServerClient(tester.PayTester.ServerUri, apiKeyServer); - var clientInsufficient= new BTCPayServerClient(tester.PayTester.ServerUri, apiKeyInsufficient); - + var clientProfile = await user.CreateClient(Permissions.ProfileManagement); + var clientServer = await user.CreateClient(Permissions.ServerManagement); + var clientInsufficient = await user.CreateClient(Permissions.StoreManagement); + + var apiKeyProfileUserData = await clientProfile.GetCurrentUser(); Assert.NotNull(apiKeyProfileUserData); Assert.Equal(apiKeyProfileUserData.Id, user.UserId); @@ -77,25 +74,5 @@ namespace BTCPayServer.Tests await clientServer.GetCurrentUser(); } } - - private static async Task GenerateAPIKey(ServerTester tester, TestAccount user, params string[] permissions) - { - var manageController = tester.PayTester.GetController(user.UserId, user.StoreId, user.IsAdmin); - var x = Assert.IsType(await manageController.AddApiKey( - new ManageController.AddApiKeyViewModel() - { - PermissionValues = permissions.Select(s => new ManageController.AddApiKeyViewModel.PermissionValueItem() - { - Permission = s, - Value = true - }).ToList(), - StoreMode = ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores - })); - var statusMessage = manageController.TempData.GetStatusMessageModel(); - Assert.NotNull(statusMessage); - var apiKey = statusMessage.Html.Substring(statusMessage.Html.IndexOf("") + 6); - apiKey = apiKey.Substring(0, apiKey.IndexOf("") ); - return apiKey; - } } } diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index 4c057e471..44769d816 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -20,6 +20,7 @@ using BTCPayServer.Lightning.CLightning; using BTCPayServer.Data; using Microsoft.AspNetCore.Identity; using NBXplorer.Models; +using BTCPayServer.Client; namespace BTCPayServer.Tests { @@ -45,6 +46,26 @@ namespace BTCPayServer.Tests IsAdmin = true; } + public async Task CreateClient(params string[] permissions) + { + var manageController = parent.PayTester.GetController(UserId, StoreId, IsAdmin); + var x = Assert.IsType(await manageController.AddApiKey( + new ManageController.AddApiKeyViewModel() + { + PermissionValues = permissions.Select(s => new ManageController.AddApiKeyViewModel.PermissionValueItem() + { + Permission = s, + Value = true + }).ToList(), + StoreMode = ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores + })); + var statusMessage = manageController.TempData.GetStatusMessageModel(); + Assert.NotNull(statusMessage); + var apiKey = statusMessage.Html.Substring(statusMessage.Html.IndexOf("") + 6); + apiKey = apiKey.Substring(0, apiKey.IndexOf("")); + return new BTCPayServerClient(parent.PayTester.ServerUri, apiKey); + } + public void Register() { RegisterAsync().GetAwaiter().GetResult(); From c85fb3e89f28ea4109f52f50146aed338bb35df7 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 17 Mar 2020 13:19:55 +0900 Subject: [PATCH 276/810] Remove ndax from test suite (crashing exchange) --- BTCPayServer.Tests/UnitTest1.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index c739427ba..cec8a16a5 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -2768,6 +2768,11 @@ noninventoryitem: { Logs.Tester.LogInformation($"Testing {result.ExpectedName}"); + if (result.ExpectedName == "ndax") + { + Logs.Tester.LogInformation($"Skipping (currently crashing)"); + continue; + } result.Fetcher.InvalidateCache(); var exchangeRates = new ExchangeRates(result.ExpectedName, result.ResultAsync.Result); result.Fetcher.InvalidateCache(); From e99767c7e22ffa2ba6e4539caf5d6d0b15ba08ce Mon Sep 17 00:00:00 2001 From: Kukks Date: Fri, 13 Mar 2020 11:47:22 +0100 Subject: [PATCH 277/810] Greenfield API: Create User Slightly big PR because I started refactoring to reduce code duplication between the UI based business logic and the api one. --- .../BTCPayServerClient.Users.cs | 8 ++ BTCPayServer.Client/BTCPayServerClient.cs | 3 +- .../Models/ApplicationUserData.cs | 34 +++++++ BTCPayServer.Tests/GreenfieldAPITests.cs | 34 +++++++ BTCPayServer/Controllers/AccountController.cs | 16 +++- BTCPayServer/Controllers/ManageController.cs | 8 +- .../RestApi/Users/UsersController.cs | 94 ++++++++++++++++++- BTCPayServer/Events/UserRegisteredEvent.cs | 12 +++ .../Extensions/UrlHelperExtensions.cs | 13 ++- .../HostedServices/UserEventHostedService.cs | 53 +++++++++++ BTCPayServer/Hosting/BTCPayServerServices.cs | 1 + BTCPayServer/Hosting/Startup.cs | 10 ++ .../AccountViewModels/RegisterViewModel.cs | 2 +- .../APIKeys/APIKeyAuthorizationHandler.cs | 8 ++ BTCPayServer/Security/Policies.cs | 6 ++ .../Services/Mails/EmailSenderFactory.cs | 2 +- 16 files changed, 282 insertions(+), 22 deletions(-) create mode 100644 BTCPayServer/Events/UserRegisteredEvent.cs create mode 100644 BTCPayServer/HostedServices/UserEventHostedService.cs diff --git a/BTCPayServer.Client/BTCPayServerClient.Users.cs b/BTCPayServer.Client/BTCPayServerClient.Users.cs index d320c3792..0879bd7ce 100644 --- a/BTCPayServer.Client/BTCPayServerClient.Users.cs +++ b/BTCPayServer.Client/BTCPayServerClient.Users.cs @@ -1,3 +1,4 @@ +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Client.Models; @@ -11,5 +12,12 @@ namespace BTCPayServer.Client var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/users/me"), token); return await HandleResponse(response); } + + public virtual async Task CreateUser(CreateApplicationUserRequest request, + CancellationToken token = default) + { + var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/users", null, request, HttpMethod.Post), token); + return await HandleResponse(response); + } } } diff --git a/BTCPayServer.Client/BTCPayServerClient.cs b/BTCPayServer.Client/BTCPayServerClient.cs index a8f97bd30..a855e1028 100644 --- a/BTCPayServer.Client/BTCPayServerClient.cs +++ b/BTCPayServer.Client/BTCPayServerClient.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; +using System.Text; using System.Text.Json; using System.Threading.Tasks; @@ -64,7 +65,7 @@ namespace BTCPayServer.Client var request = CreateHttpRequest(path, queryPayload, method); if (typeof(T).IsPrimitive || !EqualityComparer.Default.Equals(bodyPayload, default(T))) { - request.Content = new StringContent(JsonSerializer.Serialize(bodyPayload, _serializerOptions)); + request.Content = new StringContent(JsonSerializer.Serialize(bodyPayload, _serializerOptions), Encoding.UTF8, "application/json"); } return request; diff --git a/BTCPayServer.Client/Models/ApplicationUserData.cs b/BTCPayServer.Client/Models/ApplicationUserData.cs index 6965271f8..6a88944b4 100644 --- a/BTCPayServer.Client/Models/ApplicationUserData.cs +++ b/BTCPayServer.Client/Models/ApplicationUserData.cs @@ -2,7 +2,41 @@ namespace BTCPayServer.Client.Models { public class ApplicationUserData { + /// + /// the id of the user + /// public string Id { get; set; } + /// + /// the email AND username of the user + /// public string Email { get; set; } + /// + /// Whether the user has verified their email + /// + public bool EmailConfirmed { get; set; } + /// + /// whether the user needed to verify their email on account creation + /// + public bool RequiresEmailConfirmation { get; set; } + } + + public class CreateApplicationUserRequest + { + /// + /// the email AND username of the new user + /// + public string Email { get; set; } + /// + /// password of the new user + /// + public string Password { get; set; } + /// + /// Whether this user is an administrator. If left null and there are no admins in the system, the user will be created as an admin. + /// + public bool? IsAdministrator { get; set; } + /// + /// If the server requires email confirmation, this allows you to set the account as confirmed from the start + /// + public bool? EmailConfirmed { get; set; } } } diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 890f1e902..511178ca8 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -1,13 +1,17 @@ +using System; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using BTCPayServer.Client; +using BTCPayServer.Client.Models; using BTCPayServer.Controllers; +using BTCPayServer.Controllers.RestApi.Users; using BTCPayServer.Tests.Logging; using Microsoft.AspNet.SignalR.Client; using Microsoft.AspNetCore.Mvc; using Xunit; using Xunit.Abstractions; +using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest; namespace BTCPayServer.Tests { @@ -72,6 +76,36 @@ namespace BTCPayServer.Tests await Assert.ThrowsAsync(async () => await clientInsufficient.GetCurrentUser()); await clientServer.GetCurrentUser(); + + + await Assert.ThrowsAsync(async () => await clientInsufficient.CreateUser(new CreateApplicationUserRequest() + { + Email = $"{Guid.NewGuid()}@g.com", + Password = Guid.NewGuid().ToString() + }) ); + + var newUser = await clientServer.CreateUser(new CreateApplicationUserRequest() + { + Email = $"{Guid.NewGuid()}@g.com", Password = Guid.NewGuid().ToString() + }); + Assert.NotNull(newUser); + + await Assert.ThrowsAsync(async () => await clientServer.CreateUser(new CreateApplicationUserRequest() + { + Email = $"{Guid.NewGuid()}", + Password = Guid.NewGuid().ToString() + }) ); + + await Assert.ThrowsAsync(async () => await clientServer.CreateUser(new CreateApplicationUserRequest() + { + Email = $"{Guid.NewGuid()}@g.com", + }) ); + + await Assert.ThrowsAsync(async () => await clientServer.CreateUser(new CreateApplicationUserRequest() + { + Password = Guid.NewGuid().ToString() + }) ); + } } } diff --git a/BTCPayServer/Controllers/AccountController.cs b/BTCPayServer/Controllers/AccountController.cs index acaac444f..d4c0fb68a 100644 --- a/BTCPayServer/Controllers/AccountController.cs +++ b/BTCPayServer/Controllers/AccountController.cs @@ -23,6 +23,7 @@ using BTCPayServer.U2F.Models; using Newtonsoft.Json; using NicolasDorier.RateLimits; using BTCPayServer.Data; +using BTCPayServer.Events; using U2F.Core.Exceptions; namespace BTCPayServer.Controllers @@ -40,6 +41,7 @@ namespace BTCPayServer.Controllers Configuration.BTCPayServerOptions _Options; private readonly BTCPayServerEnvironment _btcPayServerEnvironment; public U2FService _u2FService; + private readonly EventAggregator _eventAggregator; ILogger _logger; public AccountController( @@ -51,7 +53,8 @@ namespace BTCPayServer.Controllers SettingsRepository settingsRepository, Configuration.BTCPayServerOptions options, BTCPayServerEnvironment btcPayServerEnvironment, - U2FService u2FService) + U2FService u2FService, + EventAggregator eventAggregator) { this.storeRepository = storeRepository; _userManager = userManager; @@ -62,6 +65,7 @@ namespace BTCPayServer.Controllers _Options = options; _btcPayServerEnvironment = btcPayServerEnvironment; _u2FService = u2FService; + _eventAggregator = eventAggregator; _logger = Logs.PayServer; } @@ -439,7 +443,6 @@ namespace BTCPayServer.Controllers if (result.Succeeded) { var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin); - Logs.PayServer.LogInformation($"A new user just registered {user.Email} {(admin.Count == 0 ? "(admin)" : "")}"); if (admin.Count == 0 || (model.IsAdmin && _Options.AllowAdminRegistration)) { await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin)); @@ -456,11 +459,14 @@ namespace BTCPayServer.Controllers RegisteredAdmin = true; } - var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); - var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme); + _eventAggregator.Publish(new UserRegisteredEvent() + { + Request = Request, + User = user, + Admin = RegisteredAdmin + }); RegisteredUserId = user.Id; - _EmailSenderFactory.GetEmailSender().SendEmailConfirmation(model.Email, callbackUrl); if (!policies.RequiresConfirmedEmail) { if (logon) diff --git a/BTCPayServer/Controllers/ManageController.cs b/BTCPayServer/Controllers/ManageController.cs index 357f96e8e..5fdaae147 100644 --- a/BTCPayServer/Controllers/ManageController.cs +++ b/BTCPayServer/Controllers/ManageController.cs @@ -20,6 +20,7 @@ using BTCPayServer.Security; using BTCPayServer.U2F; using BTCPayServer.Data; using BTCPayServer.Security.APIKeys; +using Microsoft.AspNetCore.Routing; namespace BTCPayServer.Controllers @@ -38,6 +39,7 @@ namespace BTCPayServer.Controllers private readonly BTCPayServerEnvironment _btcPayServerEnvironment; private readonly APIKeyRepository _apiKeyRepository; private readonly IAuthorizationService _authorizationService; + private readonly LinkGenerator _linkGenerator; StoreRepository _StoreRepository; @@ -54,7 +56,8 @@ namespace BTCPayServer.Controllers U2FService u2FService, BTCPayServerEnvironment btcPayServerEnvironment, APIKeyRepository apiKeyRepository, - IAuthorizationService authorizationService + IAuthorizationService authorizationService, + LinkGenerator linkGenerator ) { _userManager = userManager; @@ -67,6 +70,7 @@ namespace BTCPayServer.Controllers _btcPayServerEnvironment = btcPayServerEnvironment; _apiKeyRepository = apiKeyRepository; _authorizationService = authorizationService; + _linkGenerator = linkGenerator; _StoreRepository = storeRepository; } @@ -156,7 +160,7 @@ namespace BTCPayServer.Controllers } var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); - var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme); + var callbackUrl = _linkGenerator.EmailConfirmationLink(user.Id, code, Request.Scheme, Request.HttpContext); var email = user.Email; _EmailSenderFactory.GetEmailSender().SendEmailConfirmation(email, callbackUrl); TempData[WellKnownTempData.SuccessMessage] = "Verification email sent. Please check your email."; diff --git a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs index f0bce9ec3..c8a81d7e4 100644 --- a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs +++ b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs @@ -1,12 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using System.Linq; using System.Threading.Tasks; using BTCPayServer.Client.Models; +using BTCPayServer.Configuration; using BTCPayServer.Data; +using BTCPayServer.Events; using BTCPayServer.Hosting.OpenApi; using BTCPayServer.Security; +using BTCPayServer.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; using NSwag.Annotations; namespace BTCPayServer.Controllers.RestApi.Users @@ -18,14 +24,24 @@ namespace BTCPayServer.Controllers.RestApi.Users public class UsersController : ControllerBase { private readonly UserManager _userManager; + private readonly BTCPayServerOptions _btcPayServerOptions; + private readonly RoleManager _roleManager; + private readonly SettingsRepository _settingsRepository; + private readonly EventAggregator _eventAggregator; - public UsersController(UserManager userManager) + public UsersController(UserManager userManager, BTCPayServerOptions btcPayServerOptions, + RoleManager roleManager, SettingsRepository settingsRepository, + EventAggregator eventAggregator) { _userManager = userManager; + _btcPayServerOptions = btcPayServerOptions; + _roleManager = roleManager; + _settingsRepository = settingsRepository; + _eventAggregator = eventAggregator; } - + [OpenApiOperation("Get current user information", "View information about the current user")] - [SwaggerResponse(StatusCodes.Status200OK, typeof(ApiKeyData), + [SwaggerResponse(StatusCodes.Status200OK, typeof(ApplicationUserData), Description = "Information about the current user")] [Authorize(Policy = Policies.CanModifyProfile.Key, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] [HttpGet("~/api/v1/users/me")] @@ -34,14 +50,82 @@ namespace BTCPayServer.Controllers.RestApi.Users var user = await _userManager.GetUserAsync(User); return FromModel(user); } - + + [OpenApiOperation("Create user", "Create a new user")] + [SwaggerResponse(StatusCodes.Status201Created, typeof(ApplicationUserData), + Description = "Information about the new user")] + [SwaggerResponse(StatusCodes.Status422UnprocessableEntity, typeof(ValidationProblemDetails), + Description = "A list of validation errors that occurred")] + [SwaggerResponse(StatusCodes.Status400BadRequest, typeof(ValidationProblemDetails), + Description = "A list of errors that occurred when creating the user")] + [Authorize(Policy = Policies.CanCreateUser.Key, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [HttpPost("~/api/v1/users")] + public async Task> CreateUser(CreateApplicationUserRequest request) + { + var policies = await _settingsRepository.GetSettingAsync() ?? new PoliciesSettings(); + var anyAdmin = (await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin)).Any(); + var admin = request.IsAdministrator.GetValueOrDefault(!anyAdmin); + var user = new ApplicationUser + { + UserName = request.Email, + Email = request.Email, + RequiresEmailConfirmation = policies.RequiresConfirmedEmail, + EmailConfirmed = request.EmailConfirmed.GetValueOrDefault(false) + }; + var identityResult = await _userManager.CreateAsync(user); + if (!identityResult.Succeeded) + { + AddErrors(identityResult); + return BadRequest(new ValidationProblemDetails(ModelState)); + } + else if (admin) + { + await _roleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin)); + await _userManager.AddToRoleAsync(user, Roles.ServerAdmin); + } + + _eventAggregator.Publish(new UserRegisteredEvent() {Request = Request, User = user, Admin = admin}); + + return CreatedAtAction("", user); + } + private static ApplicationUserData FromModel(ApplicationUser data) { return new ApplicationUserData() { Id = data.Id, - Email = data.Email + Email = data.Email, + EmailConfirmed = data.EmailConfirmed, + RequiresEmailConfirmation = data.RequiresEmailConfirmation }; } + + private void AddErrors(IdentityResult result) + { + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + } + + [ModelMetadataType(typeof(CreateApplicationUserRequestMetadata))] + public class CreateApplicationUserRequest : BTCPayServer.Client.Models.CreateApplicationUserRequest + { + + } + + public class CreateApplicationUserRequestMetadata + { + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } } } diff --git a/BTCPayServer/Events/UserRegisteredEvent.cs b/BTCPayServer/Events/UserRegisteredEvent.cs new file mode 100644 index 000000000..0b14857c6 --- /dev/null +++ b/BTCPayServer/Events/UserRegisteredEvent.cs @@ -0,0 +1,12 @@ +using BTCPayServer.Data; +using Microsoft.AspNetCore.Http; + +namespace BTCPayServer.Events +{ + public class UserRegisteredEvent + { + public ApplicationUser User { get; set; } + public HttpRequest Request { get; set; } + public bool Admin { get; set; } + } +} diff --git a/BTCPayServer/Extensions/UrlHelperExtensions.cs b/BTCPayServer/Extensions/UrlHelperExtensions.cs index 1167db26e..02b543c9d 100644 --- a/BTCPayServer/Extensions/UrlHelperExtensions.cs +++ b/BTCPayServer/Extensions/UrlHelperExtensions.cs @@ -1,20 +1,19 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Controllers; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Mvc { public static class UrlHelperExtensions { - public static string EmailConfirmationLink(this IUrlHelper urlHelper, string userId, string code, string scheme) + public static string EmailConfirmationLink(this LinkGenerator urlHelper, string userId, string code, string scheme, HttpContext context) { - return urlHelper.Action( - action: nameof(AccountController.ConfirmEmail), - controller: "Account", - values: new { userId, code }, - protocol: scheme); + return urlHelper.GetUriByAction(context, nameof(AccountController.ConfirmEmail), "Account", + new {userId, code}, scheme); } public static string ResetPasswordCallbackLink(this IUrlHelper urlHelper, string userId, string code, string scheme) diff --git a/BTCPayServer/HostedServices/UserEventHostedService.cs b/BTCPayServer/HostedServices/UserEventHostedService.cs new file mode 100644 index 000000000..246370562 --- /dev/null +++ b/BTCPayServer/HostedServices/UserEventHostedService.cs @@ -0,0 +1,53 @@ +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Events; +using BTCPayServer.Logging; +using BTCPayServer.Services; +using BTCPayServer.Services.Mails; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; + +namespace BTCPayServer.HostedServices +{ + public class UserEventHostedService : EventHostedServiceBase + { + private readonly UserManager _userManager; + private readonly EmailSenderFactory _emailSenderFactory; + private readonly LinkGenerator _generator; + + public UserEventHostedService(EventAggregator eventAggregator, UserManager userManager, + EmailSenderFactory emailSenderFactory, LinkGenerator generator) : base(eventAggregator) + { + _userManager = userManager; + _emailSenderFactory = emailSenderFactory; + _generator = generator; + } + + protected override void SubscibeToEvents() + { + Subscribe(); + } + + protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) + { + switch (evt) + { + case UserRegisteredEvent userRegisteredEvent: + Logs.PayServer.LogInformation($"A new user just registered {userRegisteredEvent.User.Email} {(userRegisteredEvent.Admin ? "(admin)" : "")}"); + if (!userRegisteredEvent.User.EmailConfirmed && userRegisteredEvent.User.RequiresEmailConfirmation) + { + var code = await _userManager.GenerateEmailConfirmationTokenAsync(userRegisteredEvent.User); + var callbackUrl = _generator.EmailConfirmationLink(userRegisteredEvent.User.Id, code, userRegisteredEvent.Request.Scheme, userRegisteredEvent.Request.HttpContext); + + _emailSenderFactory.GetEmailSender() + .SendEmailConfirmation(userRegisteredEvent.User.Email, callbackUrl); + } + break; + } + } + } +} diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 602bd5343..07d8a439f 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -204,6 +204,7 @@ namespace BTCPayServer.Hosting services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index 61f0cdf06..1ba9f873c 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -72,6 +72,16 @@ namespace BTCPayServer.Hosting // ScriptSrc = "'self' 'unsafe-inline'" //}); }) + .ConfigureApiBehaviorOptions(options => + { + var builtInFactory = options.InvalidModelStateResponseFactory; + + options.InvalidModelStateResponseFactory = context => + { + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.UnprocessableEntity; + return builtInFactory(context); + }; + }) .AddNewtonsoftJson() #if DEBUG .AddRazorRuntimeCompilation() diff --git a/BTCPayServer/Models/AccountViewModels/RegisterViewModel.cs b/BTCPayServer/Models/AccountViewModels/RegisterViewModel.cs index c59e7eccf..a865c4387 100644 --- a/BTCPayServer/Models/AccountViewModels/RegisterViewModel.cs +++ b/BTCPayServer/Models/AccountViewModels/RegisterViewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; diff --git a/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs b/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs index 71c27e698..391769361 100644 --- a/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs +++ b/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs @@ -29,6 +29,13 @@ namespace BTCPayServer.Security.APIKeys protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement) { + //if it is a create user request, and the auth is not specified, and there are no admins in the system: authorize + if (context.User.Identity.AuthenticationType == null && requirement.Policy == Policies.CanCreateUser.Key && + !(await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin)).Any()) + { + context.Succeed(requirement); + } + if (context.User.Identity.AuthenticationType != APIKeyConstants.AuthenticationType) return; @@ -67,6 +74,7 @@ namespace BTCPayServer.Security.APIKeys } break; + case Policies.CanCreateUser.Key: case Policies.CanModifyServerSettings.Key: if (!context.HasPermissions(Permissions.ServerManagement)) break; diff --git a/BTCPayServer/Security/Policies.cs b/BTCPayServer/Security/Policies.cs index 9ba065d07..5e48517c1 100644 --- a/BTCPayServer/Security/Policies.cs +++ b/BTCPayServer/Security/Policies.cs @@ -13,6 +13,7 @@ namespace BTCPayServer.Security options.AddPolicy(CanModifyServerSettings.Key); options.AddPolicy(CanModifyServerSettings.Key); options.AddPolicy(CanModifyProfile.Key); + options.AddPolicy(CanCreateUser.Key); return options; } @@ -46,5 +47,10 @@ namespace BTCPayServer.Security { public const string Key = "btcpay.store.cangetrates"; } + + public class CanCreateUser + { + public const string Key = "btcpay.store.cancreateuser"; + } } } diff --git a/BTCPayServer/Services/Mails/EmailSenderFactory.cs b/BTCPayServer/Services/Mails/EmailSenderFactory.cs index 12c288350..4f88bdd4b 100644 --- a/BTCPayServer/Services/Mails/EmailSenderFactory.cs +++ b/BTCPayServer/Services/Mails/EmailSenderFactory.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Threading.Tasks; using BTCPayServer.Services.Stores; From 6e1f3989e8015af401998ba3ff76bf4d1ff66261 Mon Sep 17 00:00:00 2001 From: Kukks Date: Wed, 18 Mar 2020 08:10:35 +0100 Subject: [PATCH 278/810] remove special case --- .../Security/APIKeys/APIKeyAuthorizationHandler.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs b/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs index 391769361..c693c2be1 100644 --- a/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs +++ b/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs @@ -29,13 +29,6 @@ namespace BTCPayServer.Security.APIKeys protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement) { - //if it is a create user request, and the auth is not specified, and there are no admins in the system: authorize - if (context.User.Identity.AuthenticationType == null && requirement.Policy == Policies.CanCreateUser.Key && - !(await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin)).Any()) - { - context.Succeed(requirement); - } - if (context.User.Identity.AuthenticationType != APIKeyConstants.AuthenticationType) return; From e351e0c9ea7c157e7d2666072ada0fb2ff505dbd Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Wed, 18 Mar 2020 20:08:09 +0900 Subject: [PATCH 279/810] Remove dependency on NSwag --- BTCPayServer.Tests/BTCPayServer.Tests.csproj | 1 + BTCPayServer.Tests/UnitTest1.cs | 16 +- BTCPayServer/BTCPayServer.csproj | 3 +- BTCPayServer/Controllers/HomeController.cs | 28 ++ .../Controllers/ManageController.APIKeys.cs | 10 - .../RestApi/ApiKeys/ApiKeysController.cs | 12 +- .../RestApi/Users/UsersController.cs | 14 - BTCPayServer/Hosting/BTCPayServerServices.cs | 3 - .../Hosting/OpenApi/IncludeInOpenApiDocs.cs | 9 - .../Hosting/OpenApi/OpenApiExtensions.cs | 115 ----- BTCPayServer/Hosting/Startup.cs | 2 - BTCPayServer/Views/Home/SwaggerDocs.cshtml | 27 ++ .../wwwroot/swagger/v1/swagger.template.json | 430 ++++++++++++++++++ 13 files changed, 504 insertions(+), 166 deletions(-) delete mode 100644 BTCPayServer/Hosting/OpenApi/IncludeInOpenApiDocs.cs delete mode 100644 BTCPayServer/Hosting/OpenApi/OpenApiExtensions.cs create mode 100644 BTCPayServer/Views/Home/SwaggerDocs.cshtml create mode 100644 BTCPayServer/wwwroot/swagger/v1/swagger.template.json diff --git a/BTCPayServer.Tests/BTCPayServer.Tests.csproj b/BTCPayServer.Tests/BTCPayServer.Tests.csproj index 689f2ed40..c3b878f6d 100644 --- a/BTCPayServer.Tests/BTCPayServer.Tests.csproj +++ b/BTCPayServer.Tests/BTCPayServer.Tests.csproj @@ -24,6 +24,7 @@ + diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index cec8a16a5..8c71cd8a8 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -61,6 +61,7 @@ using NBXplorer.DerivationStrategy; using BTCPayServer.U2F.Models; using BTCPayServer.Security.Bitpay; using MemoryCache = Microsoft.Extensions.Caching.Memory.MemoryCache; +using Newtonsoft.Json.Schema; namespace BTCPayServer.Tests { @@ -90,6 +91,20 @@ namespace BTCPayServer.Tests await Task.WhenAll(checkLinks); } + [Fact] + [Trait("Fast", "Fast")] + public async Task CheckSwaggerIsConformToSchema() + { + JObject swagger = JObject.Parse(File.ReadAllText(Path.Combine(TestUtils.TryGetSolutionDirectoryInfo().FullName, "BTCPayServer", "wwwroot", "swagger", "v1", "swagger.template.json"))); + using HttpClient client = new HttpClient(); + var resp = await client.GetAsync("https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json"); + var schema = JSchema.Parse(await resp.Content.ReadAsStringAsync()); + IList errors; + bool valid = swagger.IsValid(schema, out errors); + Assert.Empty(errors); + Assert.True(valid); + } + private static async Task CheckLinks(Regex regex, HttpClient httpClient, string file) { List checkLinks = new List(); @@ -2766,7 +2781,6 @@ noninventoryitem: .Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(default), Fetcher: (BackgroundFetcherRateProvider)p.Value)) .ToList()) { - Logs.Tester.LogInformation($"Testing {result.ExpectedName}"); if (result.ExpectedName == "ndax") { diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 9d69f7fff..db100f3f6 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -48,7 +48,6 @@ - @@ -223,4 +222,6 @@ <_ContentIncludedByDefault Remove="Views\Authorization\Authorize.cshtml" /> + + diff --git a/BTCPayServer/Controllers/HomeController.cs b/BTCPayServer/Controllers/HomeController.cs index a3af749d6..1b48cd2b8 100644 --- a/BTCPayServer/Controllers/HomeController.cs +++ b/BTCPayServer/Controllers/HomeController.cs @@ -14,22 +14,30 @@ using BTCPayServer.HostedServices; using BTCPayServer.Services.Apps; using Microsoft.AspNetCore.Identity; using BTCPayServer.Data; +using Microsoft.Extensions.FileProviders; +using System.IO; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Authorization; +using BTCPayServer.Security; namespace BTCPayServer.Controllers { public class HomeController : Controller { private readonly CssThemeManager _cachedServerSettings; + private readonly IFileProvider _fileProvider; public IHttpClientFactory HttpClientFactory { get; } SignInManager SignInManager { get; } public HomeController(IHttpClientFactory httpClientFactory, CssThemeManager cachedServerSettings, + IWebHostEnvironment webHostEnvironment, SignInManager signInManager) { HttpClientFactory = httpClientFactory; _cachedServerSettings = cachedServerSettings; + _fileProvider = webHostEnvironment.WebRootFileProvider; SignInManager = signInManager; } @@ -105,6 +113,26 @@ namespace BTCPayServer.Controllers return View(new BitpayTranslatorViewModel()); } + [Route("swagger/v1/swagger.json")] + public async Task Swagger() + { + var fi = _fileProvider.GetFileInfo("swagger/v1/swagger.template.json"); + using var stream = fi.CreateReadStream(); + using var reader = new StreamReader(fi.CreateReadStream()); + var json = JObject.Parse(await reader.ReadToEndAsync()); + var servers = new JArray(); + servers.Add(new JObject(new JProperty("url", HttpContext.Request.GetAbsoluteRoot()))); + json["servers"] = servers; + return Json(json); + } + + [Route("docs")] + public IActionResult SwaggerDocs() + { + return View(); + } + + [HttpPost] [Route("translate")] public async Task BitpayTranslator(BitpayTranslatorViewModel vm) diff --git a/BTCPayServer/Controllers/ManageController.APIKeys.cs b/BTCPayServer/Controllers/ManageController.APIKeys.cs index 736fc2165..fa94b190d 100644 --- a/BTCPayServer/Controllers/ManageController.APIKeys.cs +++ b/BTCPayServer/Controllers/ManageController.APIKeys.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Threading.Tasks; using BTCPayServer.Client; using BTCPayServer.Data; -using BTCPayServer.Hosting.OpenApi; using BTCPayServer.Models; using BTCPayServer.Security; using BTCPayServer.Security.APIKeys; @@ -13,7 +12,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NBitcoin; using NBitcoin.DataEncoders; -using NSwag.Annotations; using YamlDotNet.Core.Tokens; namespace BTCPayServer.Controllers @@ -82,15 +80,7 @@ namespace BTCPayServer.Controllers return View("AddApiKey", await SetViewModelValues(new AddApiKeyViewModel())); } - /// The permissions to request. Current permissions available: ServerManagement, StoreManagement - /// The name of your application - /// If permissions are specified, and strict is set to false, it will allow the user to reject some of permissions the application is requesting. - /// If the application is requesting the CanModifyStoreSettings permission and selectiveStores is set to true, this allows the user to only grant permissions to selected stores under the user's control. [HttpGet("~/api-keys/authorize")] - [OpenApiTags("Authorization")] - [OpenApiOperation("Authorize User", - "Redirect the browser to this endpoint to request the user to generate an api-key with specific permissions")] - [IncludeInOpenApiDocs] public async Task AuthorizeAPIKey(string[] permissions, string applicationName = null, bool strict = true, bool selectiveStores = false) { diff --git a/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs b/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs index 486c048ff..688c01a42 100644 --- a/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs +++ b/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs @@ -1,20 +1,16 @@ using System.Threading.Tasks; using BTCPayServer.Client.Models; using BTCPayServer.Data; -using BTCPayServer.Hosting.OpenApi; using BTCPayServer.Security; using BTCPayServer.Security.APIKeys; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using NSwag.Annotations; namespace BTCPayServer.Controllers.RestApi.ApiKeys { [ApiController] - [IncludeInOpenApiDocs] - [OpenApiTags("API Keys")] [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKey)] public class ApiKeysController : ControllerBase { @@ -27,9 +23,6 @@ namespace BTCPayServer.Controllers.RestApi.ApiKeys _userManager = userManager; } - [OpenApiOperation("Get current API Key information", "View information about the current API key")] - [SwaggerResponse(StatusCodes.Status200OK, typeof(ApiKeyData), - Description = "Information about the current api key")] [HttpGet("~/api/v1/api-keys/current")] [HttpGet("~/api/v1/users/me/api-keys/current")] public async Task> GetKey() @@ -38,10 +31,7 @@ namespace BTCPayServer.Controllers.RestApi.ApiKeys var data = await _apiKeyRepository.GetKey(apiKey); return Ok(FromModel(data)); } - - [OpenApiOperation("Revoke the current API Key", "Revoke the current API key so that it cannot be used anymore")] - [SwaggerResponse(StatusCodes.Status200OK, typeof(ApiKeyData), - Description = "The key was revoked and is no longer usable")] + [HttpDelete("~/api/v1/api-keys/current")] [HttpDelete("~/api/v1/users/me/api-keys/current")] public async Task> RevokeKey() diff --git a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs index c8a81d7e4..718644271 100644 --- a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs +++ b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs @@ -5,7 +5,6 @@ using BTCPayServer.Client.Models; using BTCPayServer.Configuration; using BTCPayServer.Data; using BTCPayServer.Events; -using BTCPayServer.Hosting.OpenApi; using BTCPayServer.Security; using BTCPayServer.Services; using Microsoft.AspNetCore.Authorization; @@ -13,13 +12,10 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; -using NSwag.Annotations; namespace BTCPayServer.Controllers.RestApi.Users { [ApiController] - [IncludeInOpenApiDocs] - [OpenApiTags("Users")] [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKey)] public class UsersController : ControllerBase { @@ -40,9 +36,6 @@ namespace BTCPayServer.Controllers.RestApi.Users _eventAggregator = eventAggregator; } - [OpenApiOperation("Get current user information", "View information about the current user")] - [SwaggerResponse(StatusCodes.Status200OK, typeof(ApplicationUserData), - Description = "Information about the current user")] [Authorize(Policy = Policies.CanModifyProfile.Key, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] [HttpGet("~/api/v1/users/me")] public async Task> GetCurrentUser() @@ -51,13 +44,6 @@ namespace BTCPayServer.Controllers.RestApi.Users return FromModel(user); } - [OpenApiOperation("Create user", "Create a new user")] - [SwaggerResponse(StatusCodes.Status201Created, typeof(ApplicationUserData), - Description = "Information about the new user")] - [SwaggerResponse(StatusCodes.Status422UnprocessableEntity, typeof(ValidationProblemDetails), - Description = "A list of validation errors that occurred")] - [SwaggerResponse(StatusCodes.Status400BadRequest, typeof(ValidationProblemDetails), - Description = "A list of errors that occurred when creating the user")] [Authorize(Policy = Policies.CanCreateUser.Key, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] [HttpPost("~/api/v1/users")] public async Task> CreateUser(CreateApplicationUserRequest request) diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 07d8a439f..ba845c9ea 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -26,7 +26,6 @@ using System.Threading; using BTCPayServer.Services.Wallets; using BTCPayServer.Logging; using BTCPayServer.HostedServices; -using BTCPayServer.Hosting.OpenApi; using BTCPayServer.PaymentRequest; using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; @@ -264,8 +263,6 @@ namespace BTCPayServer.Hosting } return rateLimits; }); - services.AddBTCPayOpenApi(); - services.AddLogging(logBuilder => { var debugLogFile = BTCPayServerOptions.GetDebugLog(configuration); diff --git a/BTCPayServer/Hosting/OpenApi/IncludeInOpenApiDocs.cs b/BTCPayServer/Hosting/OpenApi/IncludeInOpenApiDocs.cs deleted file mode 100644 index 5921efdaf..000000000 --- a/BTCPayServer/Hosting/OpenApi/IncludeInOpenApiDocs.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - - -namespace BTCPayServer.Hosting.OpenApi -{ - public class IncludeInOpenApiDocs : Attribute - { - } -} diff --git a/BTCPayServer/Hosting/OpenApi/OpenApiExtensions.cs b/BTCPayServer/Hosting/OpenApi/OpenApiExtensions.cs deleted file mode 100644 index ece7bb57d..000000000 --- a/BTCPayServer/Hosting/OpenApi/OpenApiExtensions.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using BTCPayServer.Configuration; -using BTCPayServer.Data; -using BTCPayServer.Payments; -using BTCPayServer.Security; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.DependencyInjection; -using NJsonSchema; -using NJsonSchema.Generation.TypeMappers; -using NSwag; -using NSwag.Generation.Processors.Security; -using Org.BouncyCastle.Asn1.Ocsp; - -namespace BTCPayServer.Hosting.OpenApi -{ - public static class OpenApiExtensions - { - public static IServiceCollection AddBTCPayOpenApi(this IServiceCollection serviceCollection) - { - return serviceCollection.AddOpenApiDocument(config => - { - config.PostProcess = document => - { - document.Info.Version = "v1"; - document.Info.Title = "BTCPay Greenfield API"; - document.Info.Description = "A full API to use your BTCPay Server"; - document.Info.TermsOfService = null; - document.Info.Contact = new NSwag.OpenApiContact - { - Name = "BTCPay Server", Email = string.Empty, Url = "https://btcpayserver.org" - }; - }; - config.AddOperationFilter(context => - { - var methodInfo = context.MethodInfo; - if (methodInfo != null) - { - return methodInfo.CustomAttributes.Any(data => - data.AttributeType == typeof(IncludeInOpenApiDocs)) || - methodInfo.DeclaringType.CustomAttributes.Any(data => - data.AttributeType == typeof(IncludeInOpenApiDocs)); - } - - return false; - }); - - config.AddSecurity("APIKey", Enumerable.Empty(), - new OpenApiSecurityScheme - { - Type = OpenApiSecuritySchemeType.ApiKey, - Name = "Authorization", - In = OpenApiSecurityApiKeyLocation.Header, - Description = - "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: token {token}. For a smoother experience, you can generate a url that redirects users to an API key creation screen." - }); - - config.OperationProcessors.Add( - new BTCPayPolicyOperationProcessor("APIKey", AuthenticationSchemes.ApiKey)); - - config.TypeMappers.Add( - new PrimitiveTypeMapper(typeof(PaymentType), s => s.Type = JsonObjectType.String)); - config.TypeMappers.Add(new PrimitiveTypeMapper(typeof(PaymentMethodId), - s => s.Type = JsonObjectType.String)); - }); - } - - public static IApplicationBuilder UseBTCPayOpenApi(this IApplicationBuilder builder) - { - var roothPath = builder.ApplicationServices.GetService().RootPath; - var matched = new PathString($"{roothPath}docs"); - return builder.UseOpenApi() - .Use(async (context, next) => - { - if (context.Request.Path.StartsWithSegments(matched, StringComparison.InvariantCultureIgnoreCase) && !context.User.Claims.Any()) - { - context.Response.Redirect( $"{context.Request.GetRelativePath(roothPath)}account/login?returnUrl={context.Request.Path}"); - return; - } - - await next.Invoke(); - }) - .UseReDoc(settings => - { - settings.Path = "/docs"; - }); - } - - - class BTCPayPolicyOperationProcessor : AspNetCoreOperationSecurityScopeProcessor - { - private readonly string _authScheme; - - public BTCPayPolicyOperationProcessor(string x, string authScheme) : base(x) - { - _authScheme = authScheme; - } - - protected override IEnumerable GetScopes(IEnumerable authorizeAttributes) - { - var result = authorizeAttributes - .Where(attribute => attribute?.AuthenticationSchemes != null && attribute.Policy != null && - attribute.AuthenticationSchemes.Equals(_authScheme, - StringComparison.InvariantCultureIgnoreCase)) - .Select(attribute => attribute.Policy); - - return result; - } - } - } -} diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index 1ba9f873c..9dc0221b8 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -19,7 +19,6 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using BTCPayServer.Security; using Microsoft.AspNetCore.Server.Kestrel.Core; using System.Net; -using BTCPayServer.Hosting.OpenApi; using BTCPayServer.PaymentRequest; using BTCPayServer.Services.Apps; using BTCPayServer.Storage; @@ -194,7 +193,6 @@ namespace BTCPayServer.Hosting app.UseProviderStorage(options); app.UseAuthentication(); app.UseAuthorization(); - app.UseBTCPayOpenApi(); app.UseSession(); app.UseWebSockets(); diff --git a/BTCPayServer/Views/Home/SwaggerDocs.cshtml b/BTCPayServer/Views/Home/SwaggerDocs.cshtml new file mode 100644 index 000000000..00c2bcaf0 --- /dev/null +++ b/BTCPayServer/Views/Home/SwaggerDocs.cshtml @@ -0,0 +1,27 @@ +@{ + Layout = null; +} + + + + ReDoc + + + + + + + + + + + + + diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json new file mode 100644 index 000000000..1f9a34d30 --- /dev/null +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json @@ -0,0 +1,430 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "BTCPay Greenfield API", + "description": "A full API to use your BTCPay Server", + "contact": { + "name": "BTCPay Server", + "url": "https://btcpayserver.org", + "email": "nicolas.dorier@gmail.com" + }, + "version": "v1" + }, + "servers": [ + ], + "paths": { + "/api-keys/authorize": { + "get": { + "tags": [ + "Authorization" + ], + "summary": "Authorize User", + "description": "Redirect the browser to this endpoint to request the user to generate an api-key with specific permissions", + "operationId": "Manage_AuthorizeAPIKey", + "parameters": [ + { + "name": "permissions", + "description": "The permissions to request. Current permissions available: ServerManagement, StoreManagement", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + }, + "x-position": 1 + }, + { + "name": "applicationName", + "description": "The name of your application", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 2 + }, + { + "name": "strict", + "description": "If permissions are specified, and strict is set to false, it will allow the user to reject some of permissions the application is requesting.", + "in": "query", + "schema": { + "type": "boolean", + "default": true + }, + "x-position": 3 + }, + { + "name": "selectiveStores", + "description": "If the application is requesting the CanModifyStoreSettings permission and selectiveStores is set to true, this allows the user to only grant permissions to selected stores under the user's control.", + "in": "query", + "schema": { + "type": "boolean", + "default": false + }, + "x-position": 4 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + }, + "security": [ + { + "APIKey": [] + } + ] + } + }, + "/api/v1/users/me": { + "get": { + "tags": [ + "Users" + ], + "summary": "Get current user information", + "description": "View information about the current user", + "operationId": "Users_GetCurrentUser", + "responses": { + "200": { + "description": "Information about the current user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationUserData" + } + } + } + } + }, + "security": [ + { + "APIKey": [ + "btcpay.store.canmodifyprofile" + ] + } + ] + } + }, + "/api/v1/users": { + "post": { + "tags": [ + "Users" + ], + "summary": "Create user", + "description": "Create a new user", + "operationId": "Users_CreateUser", + "requestBody": { + "x-name": "request", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "email": { + "type": "string", + "nullable": true + }, + "password": { + "type": "string", + "nullable": true + }, + "isAdministrator": { + "type": "boolean", + "nullable": true + }, + "emailConfirmed": { + "type": "boolean", + "nullable": true + } + } + } + } + }, + "required": true, + "x-position": 1 + }, + "responses": { + "201": { + "description": "Information about the new user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationUserData" + } + } + } + }, + "422": { + "description": "A list of validation errors that occurred", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationProblemDetails" + } + } + } + }, + "400": { + "description": "A list of errors that occurred when creating the user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationProblemDetails" + } + } + } + } + }, + "security": [ + { + "APIKey": [ + "btcpay.store.cancreateuser" + ] + } + ] + } + }, + "/api/v1/api-keys/current": { + "get": { + "tags": [ + "API Keys" + ], + "summary": "Get current API Key information", + "description": "View information about the current API key", + "operationId": "ApiKeys_GetKey", + "responses": { + "200": { + "description": "Information about the current api key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiKeyData" + } + } + } + } + }, + "security": [ + { + "APIKey": [] + } + ] + }, + "delete": { + "tags": [ + "API Keys" + ], + "summary": "Revoke the current API Key", + "description": "Revoke the current API key so that it cannot be used anymore", + "operationId": "ApiKeys_RevokeKey", + "responses": { + "200": { + "description": "The key was revoked and is no longer usable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiKeyData" + } + } + } + } + }, + "security": [ + { + "APIKey": [] + } + ] + } + }, + "/api/v1/users/me/api-keys/current": { + "get": { + "tags": [ + "API Keys" + ], + "summary": "Get current API Key information", + "description": "View information about the current API key", + "operationId": "ApiKeys_GetKey2", + "responses": { + "200": { + "description": "Information about the current api key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiKeyData" + } + } + } + } + }, + "security": [ + { + "APIKey": [] + } + ] + }, + "delete": { + "tags": [ + "API Keys" + ], + "summary": "Revoke the current API Key", + "description": "Revoke the current API key so that it cannot be used anymore", + "operationId": "ApiKeys_RevokeKey2", + "responses": { + "200": { + "description": "The key was revoked and is no longer usable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiKeyData" + } + } + } + } + }, + "security": [ + { + "APIKey": [] + } + ] + } + } + }, + "components": { + "schemas": { + "ApplicationUserData": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "nullable": true + }, + "emailConfirmed": { + "type": "boolean" + }, + "requiresEmailConfirmation": { + "type": "boolean" + } + } + }, + "ValidationProblemDetails": { + "allOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "errors": { + "type": "object", + "nullable": true, + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + ] + }, + "ProblemDetails": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string", + "nullable": true + }, + "status": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "detail": { + "type": "string", + "nullable": true + }, + "instance": { + "type": "string", + "nullable": true + }, + "extensions": { + "type": "object", + "nullable": true, + "additionalProperties": {} + } + } + }, + "ApiKeyData": { + "type": "object", + "additionalProperties": false, + "properties": { + "apiKey": { + "type": "string", + "nullable": true + }, + "label": { + "type": "string", + "nullable": true + }, + "userId": { + "type": "string", + "nullable": true + }, + "permissions": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + } + } + } + }, + "securitySchemes": { + "APIKey": { + "type": "apiKey", + "description": "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: token {token}. For a smoother experience, you can generate a url that redirects users to an API key creation screen.", + "name": "Authorization", + "in": "header" + } + } + }, + "security": [ + { + "APIKey": [] + } + ], + "tags": [ + { + "name": "Users" + }, + { + "name": "API Keys" + } + ] +} From 132c36df7b3763ac0d1f95d6dc6033f572da93fe Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Wed, 18 Mar 2020 20:25:54 +0900 Subject: [PATCH 280/810] Remove useless routes and info in swagger --- .../RestApi/ApiKeys/ApiKeysController.cs | 2 - .../wwwroot/swagger/v1/swagger.template.json | 134 +----------------- 2 files changed, 1 insertion(+), 135 deletions(-) diff --git a/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs b/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs index 688c01a42..ab229a6f7 100644 --- a/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs +++ b/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs @@ -24,7 +24,6 @@ namespace BTCPayServer.Controllers.RestApi.ApiKeys } [HttpGet("~/api/v1/api-keys/current")] - [HttpGet("~/api/v1/users/me/api-keys/current")] public async Task> GetKey() { ControllerContext.HttpContext.GetAPIKey(out var apiKey); @@ -33,7 +32,6 @@ namespace BTCPayServer.Controllers.RestApi.ApiKeys } [HttpDelete("~/api/v1/api-keys/current")] - [HttpDelete("~/api/v1/users/me/api-keys/current")] public async Task> RevokeKey() { ControllerContext.HttpContext.GetAPIKey(out var apiKey); diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json index 1f9a34d30..17c6cbcfc 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json @@ -20,11 +20,10 @@ ], "summary": "Authorize User", "description": "Redirect the browser to this endpoint to request the user to generate an api-key with specific permissions", - "operationId": "Manage_AuthorizeAPIKey", "parameters": [ { "name": "permissions", - "description": "The permissions to request. Current permissions available: ServerManagement, StoreManagement", + "description": "The permissions to request. Current permissions available: ServerManagement, StoreManagement, ProfileManagement", "in": "query", "style": "form", "explode": true, @@ -95,7 +94,6 @@ ], "summary": "Get current user information", "description": "View information about the current user", - "operationId": "Users_GetCurrentUser", "responses": { "200": { "description": "Information about the current user", @@ -124,7 +122,6 @@ ], "summary": "Create user", "description": "Create a new user", - "operationId": "Users_CreateUser", "requestBody": { "x-name": "request", "content": { @@ -166,26 +163,6 @@ } } } - }, - "422": { - "description": "A list of validation errors that occurred", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ValidationProblemDetails" - } - } - } - }, - "400": { - "description": "A list of errors that occurred when creating the user", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ValidationProblemDetails" - } - } - } } }, "security": [ @@ -204,7 +181,6 @@ ], "summary": "Get current API Key information", "description": "View information about the current API key", - "operationId": "ApiKeys_GetKey", "responses": { "200": { "description": "Information about the current api key", @@ -229,59 +205,6 @@ ], "summary": "Revoke the current API Key", "description": "Revoke the current API key so that it cannot be used anymore", - "operationId": "ApiKeys_RevokeKey", - "responses": { - "200": { - "description": "The key was revoked and is no longer usable", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiKeyData" - } - } - } - } - }, - "security": [ - { - "APIKey": [] - } - ] - } - }, - "/api/v1/users/me/api-keys/current": { - "get": { - "tags": [ - "API Keys" - ], - "summary": "Get current API Key information", - "description": "View information about the current API key", - "operationId": "ApiKeys_GetKey2", - "responses": { - "200": { - "description": "Information about the current api key", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiKeyData" - } - } - } - } - }, - "security": [ - { - "APIKey": [] - } - ] - }, - "delete": { - "tags": [ - "API Keys" - ], - "summary": "Revoke the current API Key", - "description": "Revoke the current API key so that it cannot be used anymore", - "operationId": "ApiKeys_RevokeKey2", "responses": { "200": { "description": "The key was revoked and is no longer usable", @@ -324,61 +247,6 @@ } } }, - "ValidationProblemDetails": { - "allOf": [ - { - "$ref": "#/components/schemas/ProblemDetails" - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "errors": { - "type": "object", - "nullable": true, - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - ] - }, - "ProblemDetails": { - "type": "object", - "additionalProperties": false, - "properties": { - "type": { - "type": "string", - "nullable": true - }, - "title": { - "type": "string", - "nullable": true - }, - "status": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "detail": { - "type": "string", - "nullable": true - }, - "instance": { - "type": "string", - "nullable": true - }, - "extensions": { - "type": "object", - "nullable": true, - "additionalProperties": {} - } - } - }, "ApiKeyData": { "type": "object", "additionalProperties": false, From 540a31207e1a18b806cb80bb562e8f2f347826e0 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Wed, 18 Mar 2020 20:51:50 +0900 Subject: [PATCH 281/810] Properly validate create user input --- .../RestApi/Users/UsersController.cs | 67 +++++----- .../wwwroot/swagger/v1/swagger.template.json | 124 +++++++++++++++++- 2 files changed, 157 insertions(+), 34 deletions(-) diff --git a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs index 718644271..f1980358e 100644 --- a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs +++ b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs @@ -1,3 +1,4 @@ +using System; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; @@ -24,16 +25,19 @@ namespace BTCPayServer.Controllers.RestApi.Users private readonly RoleManager _roleManager; private readonly SettingsRepository _settingsRepository; private readonly EventAggregator _eventAggregator; + private readonly IPasswordValidator _passwordValidator; public UsersController(UserManager userManager, BTCPayServerOptions btcPayServerOptions, RoleManager roleManager, SettingsRepository settingsRepository, - EventAggregator eventAggregator) + EventAggregator eventAggregator, + IPasswordValidator passwordValidator) { _userManager = userManager; _btcPayServerOptions = btcPayServerOptions; _roleManager = roleManager; _settingsRepository = settingsRepository; _eventAggregator = eventAggregator; + _passwordValidator = passwordValidator; } [Authorize(Policy = Policies.CanModifyProfile.Key, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] @@ -48,6 +52,14 @@ namespace BTCPayServer.Controllers.RestApi.Users [HttpPost("~/api/v1/users")] public async Task> CreateUser(CreateApplicationUserRequest request) { + if (request?.Email is null) + return BadRequest(CreateValidationProblem(nameof(request.Email), "Email is missing")); + if (!Validation.EmailValidator.IsEmail(request.Email)) + { + return BadRequest(CreateValidationProblem(nameof(request.Email), "Invalid email")); + } + if (request?.Password is null) + return BadRequest(CreateValidationProblem(nameof(request.Password), "Password is missing")); var policies = await _settingsRepository.GetSettingAsync() ?? new PoliciesSettings(); var anyAdmin = (await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin)).Any(); var admin = request.IsAdministrator.GetValueOrDefault(!anyAdmin); @@ -58,10 +70,22 @@ namespace BTCPayServer.Controllers.RestApi.Users RequiresEmailConfirmation = policies.RequiresConfirmedEmail, EmailConfirmed = request.EmailConfirmed.GetValueOrDefault(false) }; - var identityResult = await _userManager.CreateAsync(user); + var passwordValidation = await this._passwordValidator.ValidateAsync(_userManager, user, request.Password); + if (!passwordValidation.Succeeded) + { + foreach (var error in passwordValidation.Errors) + { + ModelState.AddModelError(nameof(request.Password), error.Description); + } + return BadRequest(new ValidationProblemDetails(ModelState)); + } + var identityResult = await _userManager.CreateAsync(user, request.Password); if (!identityResult.Succeeded) { - AddErrors(identityResult); + foreach (var error in identityResult.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } return BadRequest(new ValidationProblemDetails(ModelState)); } else if (admin) @@ -69,12 +93,17 @@ namespace BTCPayServer.Controllers.RestApi.Users await _roleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin)); await _userManager.AddToRoleAsync(user, Roles.ServerAdmin); } - _eventAggregator.Publish(new UserRegisteredEvent() {Request = Request, User = user, Admin = admin}); - return CreatedAtAction("", user); } + private ValidationProblemDetails CreateValidationProblem(string propertyName, string errorMessage) + { + var modelState = new ModelStateDictionary(); + modelState.AddModelError(propertyName, errorMessage); + return new ValidationProblemDetails(modelState); + } + private static ApplicationUserData FromModel(ApplicationUser data) { return new ApplicationUserData() @@ -85,33 +114,5 @@ namespace BTCPayServer.Controllers.RestApi.Users RequiresEmailConfirmation = data.RequiresEmailConfirmation }; } - - private void AddErrors(IdentityResult result) - { - foreach (var error in result.Errors) - { - ModelState.AddModelError(string.Empty, error.Description); - } - } - } - - [ModelMetadataType(typeof(CreateApplicationUserRequestMetadata))] - public class CreateApplicationUserRequest : BTCPayServer.Client.Models.CreateApplicationUserRequest - { - - } - - public class CreateApplicationUserRequestMetadata - { - [Required] - [EmailAddress] - [Display(Name = "Email")] - public string Email { get; set; } - - [Required] - [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] - [DataType(DataType.Password)] - [Display(Name = "Password")] - public string Password { get; set; } } } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json index 17c6cbcfc..c421e63a8 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json @@ -20,10 +20,11 @@ ], "summary": "Authorize User", "description": "Redirect the browser to this endpoint to request the user to generate an api-key with specific permissions", + "operationId": "Manage_AuthorizeAPIKey", "parameters": [ { "name": "permissions", - "description": "The permissions to request. Current permissions available: ServerManagement, StoreManagement, ProfileManagement", + "description": "The permissions to request. Current permissions available: ServerManagement, StoreManagement", "in": "query", "style": "form", "explode": true, @@ -94,6 +95,7 @@ ], "summary": "Get current user information", "description": "View information about the current user", + "operationId": "Users_GetCurrentUser", "responses": { "200": { "description": "Information about the current user", @@ -122,6 +124,7 @@ ], "summary": "Create user", "description": "Create a new user", + "operationId": "Users_CreateUser", "requestBody": { "x-name": "request", "content": { @@ -163,6 +166,16 @@ } } } + }, + "400": { + "description": "A list of errors that occurred when creating the user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationProblemDetails" + } + } + } } }, "security": [ @@ -181,6 +194,7 @@ ], "summary": "Get current API Key information", "description": "View information about the current API key", + "operationId": "ApiKeys_GetKey", "responses": { "200": { "description": "Information about the current api key", @@ -205,6 +219,59 @@ ], "summary": "Revoke the current API Key", "description": "Revoke the current API key so that it cannot be used anymore", + "operationId": "ApiKeys_RevokeKey", + "responses": { + "200": { + "description": "The key was revoked and is no longer usable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiKeyData" + } + } + } + } + }, + "security": [ + { + "APIKey": [] + } + ] + } + }, + "/api/v1/users/me/api-keys/current": { + "get": { + "tags": [ + "API Keys" + ], + "summary": "Get current API Key information", + "description": "View information about the current API key", + "operationId": "ApiKeys_GetKey2", + "responses": { + "200": { + "description": "Information about the current api key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiKeyData" + } + } + } + } + }, + "security": [ + { + "APIKey": [] + } + ] + }, + "delete": { + "tags": [ + "API Keys" + ], + "summary": "Revoke the current API Key", + "description": "Revoke the current API key so that it cannot be used anymore", + "operationId": "ApiKeys_RevokeKey2", "responses": { "200": { "description": "The key was revoked and is no longer usable", @@ -247,6 +314,61 @@ } } }, + "ValidationProblemDetails": { + "allOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "errors": { + "type": "object", + "nullable": true, + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + ] + }, + "ProblemDetails": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string", + "nullable": true + }, + "status": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "detail": { + "type": "string", + "nullable": true + }, + "instance": { + "type": "string", + "nullable": true + }, + "extensions": { + "type": "object", + "nullable": true, + "additionalProperties": {} + } + } + }, "ApiKeyData": { "type": "object", "additionalProperties": false, From e50e3f662d0cfe00de0339e21e7fedd556133d81 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Wed, 18 Mar 2020 23:10:15 +0900 Subject: [PATCH 282/810] Can create user without authentication if there is no admin --- BTCPayServer.Client/BTCPayServerClient.cs | 10 +- .../Models/ApplicationUserData.cs | 4 - BTCPayServer.Tests/GreenfieldAPITests.cs | 102 +++++++++++++++--- BTCPayServer.Tests/ServerTester.cs | 12 ++- BTCPayServer/Controllers/AccountController.cs | 1 + .../RestApi/Users/UsersController.cs | 70 ++++++++++-- BTCPayServer/Hosting/BTCPayServerServices.cs | 2 + BTCPayServer/ZoneLimits.cs | 1 + .../wwwroot/swagger/v1/swagger.template.json | 33 +++--- 9 files changed, 189 insertions(+), 46 deletions(-) diff --git a/BTCPayServer.Client/BTCPayServerClient.cs b/BTCPayServer.Client/BTCPayServerClient.cs index a855e1028..60c81c400 100644 --- a/BTCPayServer.Client/BTCPayServerClient.cs +++ b/BTCPayServer.Client/BTCPayServerClient.cs @@ -23,6 +23,13 @@ namespace BTCPayServer.Client PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + public BTCPayServerClient(Uri btcpayHost, HttpClient httpClient = null) + { + if (btcpayHost == null) + throw new ArgumentNullException(nameof(btcpayHost)); + _btcpayHost = btcpayHost; + _httpClient = httpClient ?? new HttpClient(); + } public BTCPayServerClient(Uri btcpayHost, string APIKey, HttpClient httpClient = null) { _apiKey = APIKey; @@ -52,7 +59,8 @@ namespace BTCPayServer.Client } var httpRequest = new HttpRequestMessage(method ?? HttpMethod.Get, uriBuilder.Uri); - httpRequest.Headers.Authorization = new AuthenticationHeaderValue("token", _apiKey); + if (_apiKey != null) + httpRequest.Headers.Authorization = new AuthenticationHeaderValue("token", _apiKey); return httpRequest; diff --git a/BTCPayServer.Client/Models/ApplicationUserData.cs b/BTCPayServer.Client/Models/ApplicationUserData.cs index 6a88944b4..7c7dd142c 100644 --- a/BTCPayServer.Client/Models/ApplicationUserData.cs +++ b/BTCPayServer.Client/Models/ApplicationUserData.cs @@ -34,9 +34,5 @@ namespace BTCPayServer.Client.Models /// Whether this user is an administrator. If left null and there are no admins in the system, the user will be created as an admin. /// public bool? IsAdministrator { get; set; } - /// - /// If the server requires email confirmation, this allows you to set the account as confirmed from the start - /// - public bool? EmailConfirmed { get; set; } } } diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 511178ca8..1e7403fd9 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -6,6 +6,7 @@ using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Controllers; using BTCPayServer.Controllers.RestApi.Users; +using BTCPayServer.Services; using BTCPayServer.Tests.Logging; using Microsoft.AspNet.SignalR.Client; using Microsoft.AspNetCore.Mvc; @@ -23,7 +24,7 @@ namespace BTCPayServer.Tests public GreenfieldAPITests(ITestOutputHelper helper) { - Logs.Tester = new XUnitLog(helper) {Name = "Tests"}; + Logs.Tester = new XUnitLog(helper) { Name = "Tests" }; Logs.LogProvider = new XUnitLogProvider(helper); } @@ -44,7 +45,7 @@ namespace BTCPayServer.Tests Assert.Equal(client.APIKey, apiKeyData.ApiKey); Assert.Equal(user.UserId, apiKeyData.UserId); Assert.Equal(2, apiKeyData.Permissions.Length); - + //revoke current api key await client.RevokeCurrentAPIKeyInfo(); await Assert.ThrowsAsync(async () => @@ -53,7 +54,81 @@ namespace BTCPayServer.Tests }); } } - + + [Fact(Timeout = TestTimeout)] + [Trait("Integration", "Integration")] + public async Task CanCreateUsersViaAPI() + { + using (var tester = ServerTester.Create(newDb: true)) + { + await tester.StartAsync(); + var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri); + await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest())); + await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test@gmail.com" })); + // Pass too simple + await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "a" })); + + // We have no admin, so it should work + var user1 = await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test@gmail.com", Password = "abceudhqw" }); + // We have no admin, so it should work + var user2 = await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" }); + + // Duplicate email + await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" })); + + // Let's make an admin + var admin = await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin@gmail.com", Password = "abceudhqw", IsAdministrator = true }); + + // Creating a new user without proper creds is now impossible (unauthorized) + // Because if registration are locked and that an admin exists, we don't accept unauthenticated connection + await AssertHttpError(401, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" })); + + + // But should be ok with subscriptions unlocked + var settings = tester.PayTester.GetService(); + await settings.UpdateSetting(new PoliciesSettings() { LockSubscription = false }); + await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" }); + + // But it should be forbidden to create an admin without being authenticated + await AssertHttpError(403, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin2@gmail.com", Password = "afewfoiewiou", IsAdministrator = true })); + await settings.UpdateSetting(new PoliciesSettings() { LockSubscription = true }); + + var adminAcc = tester.NewAccount(); + adminAcc.UserId = admin.Id; + adminAcc.IsAdmin = true; + var adminClient = await adminAcc.CreateClient(Permissions.ProfileManagement); + + // We should be forbidden to create a new user without proper admin permissions + await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" })); + await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true })); + + // However, should be ok with the server management permissions + adminClient = await adminAcc.CreateClient(Permissions.ServerManagement); + await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" }); + // Even creating new admin should be ok + await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true }); + + var user1Acc = tester.NewAccount(); + user1Acc.UserId = user1.Id; + user1Acc.IsAdmin = false; + var user1Client = await user1Acc.CreateClient(Permissions.ServerManagement); + // User1 trying to get server management would still fail to create user + await AssertHttpError(403, async () => await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" })); + + // User1 should be able to create user if subscription unlocked + await settings.UpdateSetting(new PoliciesSettings() { LockSubscription = false }); + await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" }); + // But not an admin + await AssertHttpError(403, async () => await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "admin8@gmail.com", Password = "afewfoiewiou", IsAdministrator = true })); + } + } + + private async Task AssertHttpError(int code, Func act) + { + var ex = await Assert.ThrowsAsync(act); + Assert.Contains(code.ToString(), ex.Message); + } + [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task UsersControllerTests() @@ -76,35 +151,36 @@ namespace BTCPayServer.Tests await Assert.ThrowsAsync(async () => await clientInsufficient.GetCurrentUser()); await clientServer.GetCurrentUser(); - - + + await Assert.ThrowsAsync(async () => await clientInsufficient.CreateUser(new CreateApplicationUserRequest() { Email = $"{Guid.NewGuid()}@g.com", Password = Guid.NewGuid().ToString() - }) ); + })); var newUser = await clientServer.CreateUser(new CreateApplicationUserRequest() { - Email = $"{Guid.NewGuid()}@g.com", Password = Guid.NewGuid().ToString() + Email = $"{Guid.NewGuid()}@g.com", + Password = Guid.NewGuid().ToString() }); Assert.NotNull(newUser); - + await Assert.ThrowsAsync(async () => await clientServer.CreateUser(new CreateApplicationUserRequest() { Email = $"{Guid.NewGuid()}", Password = Guid.NewGuid().ToString() - }) ); - + })); + await Assert.ThrowsAsync(async () => await clientServer.CreateUser(new CreateApplicationUserRequest() { Email = $"{Guid.NewGuid()}@g.com", - }) ); - + })); + await Assert.ThrowsAsync(async () => await clientServer.CreateUser(new CreateApplicationUserRequest() { Password = Guid.NewGuid().ToString() - }) ); + })); } } diff --git a/BTCPayServer.Tests/ServerTester.cs b/BTCPayServer.Tests/ServerTester.cs index a5321e1ed..714ff14d5 100644 --- a/BTCPayServer.Tests/ServerTester.cs +++ b/BTCPayServer.Tests/ServerTester.cs @@ -29,13 +29,13 @@ namespace BTCPayServer.Tests { public class ServerTester : IDisposable { - public static ServerTester Create([CallerMemberNameAttribute]string scope = null) + public static ServerTester Create([CallerMemberNameAttribute]string scope = null, bool newDb = false) { - return new ServerTester(scope); + return new ServerTester(scope, newDb); } string _Directory; - public ServerTester(string scope) + public ServerTester(string scope, bool newDb) { _Directory = scope; if (Directory.Exists(_Directory)) @@ -59,6 +59,12 @@ namespace BTCPayServer.Tests Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"), MySQL = GetEnvironment("TESTS_MYSQL", "User ID=root;Host=127.0.0.1;Port=33036;Database=btcpayserver") }; + if (newDb) + { + var r = RandomUtils.GetUInt32(); + PayTester.Postgres = PayTester.Postgres.Replace("btcpayserver", $"btcpayserver{r}"); + PayTester.MySQL = PayTester.MySQL.Replace("btcpayserver", $"btcpayserver{r}"); + } PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture); PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1"); PayTester.InContainer = bool.Parse(GetEnvironment("TESTS_INCONTAINER", "false")); diff --git a/BTCPayServer/Controllers/AccountController.cs b/BTCPayServer/Controllers/AccountController.cs index d4c0fb68a..74b345832 100644 --- a/BTCPayServer/Controllers/AccountController.cs +++ b/BTCPayServer/Controllers/AccountController.cs @@ -404,6 +404,7 @@ namespace BTCPayServer.Controllers [HttpGet] [AllowAnonymous] + [RateLimitsFilter(ZoneLimits.Register, Scope = RateLimitsScope.RemoteAddress)] public async Task Register(string returnUrl = null, bool logon = true, bool useBasicLayout = false) { if (!CanLoginOrRegister()) diff --git a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs index f1980358e..35493547d 100644 --- a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs +++ b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs @@ -1,18 +1,21 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Threading; using System.Threading.Tasks; using BTCPayServer.Client.Models; using BTCPayServer.Configuration; using BTCPayServer.Data; using BTCPayServer.Events; using BTCPayServer.Security; +using BTCPayServer.Security.APIKeys; using BTCPayServer.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; +using NicolasDorier.RateLimits; namespace BTCPayServer.Controllers.RestApi.Users { @@ -26,11 +29,15 @@ namespace BTCPayServer.Controllers.RestApi.Users private readonly SettingsRepository _settingsRepository; private readonly EventAggregator _eventAggregator; private readonly IPasswordValidator _passwordValidator; + private readonly RateLimitService _throttleService; + private readonly IAuthorizationService _authorizationService; public UsersController(UserManager userManager, BTCPayServerOptions btcPayServerOptions, RoleManager roleManager, SettingsRepository settingsRepository, EventAggregator eventAggregator, - IPasswordValidator passwordValidator) + IPasswordValidator passwordValidator, + NicolasDorier.RateLimits.RateLimitService throttleService, + IAuthorizationService authorizationService) { _userManager = userManager; _btcPayServerOptions = btcPayServerOptions; @@ -38,6 +45,8 @@ namespace BTCPayServer.Controllers.RestApi.Users _settingsRepository = settingsRepository; _eventAggregator = eventAggregator; _passwordValidator = passwordValidator; + _throttleService = throttleService; + _authorizationService = authorizationService; } [Authorize(Policy = Policies.CanModifyProfile.Key, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] @@ -48,9 +57,9 @@ namespace BTCPayServer.Controllers.RestApi.Users return FromModel(user); } - [Authorize(Policy = Policies.CanCreateUser.Key, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [AllowAnonymous] [HttpPost("~/api/v1/users")] - public async Task> CreateUser(CreateApplicationUserRequest request) + public async Task> CreateUser(CreateApplicationUserRequest request, CancellationToken cancellationToken = default) { if (request?.Email is null) return BadRequest(CreateValidationProblem(nameof(request.Email), "Email is missing")); @@ -60,15 +69,39 @@ namespace BTCPayServer.Controllers.RestApi.Users } if (request?.Password is null) return BadRequest(CreateValidationProblem(nameof(request.Password), "Password is missing")); - var policies = await _settingsRepository.GetSettingAsync() ?? new PoliciesSettings(); var anyAdmin = (await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin)).Any(); - var admin = request.IsAdministrator.GetValueOrDefault(!anyAdmin); + var policies = await _settingsRepository.GetSettingAsync() ?? new PoliciesSettings(); + var isAuth = User.Identity.AuthenticationType == APIKeyConstants.AuthenticationType; + + // If registration are locked and that an admin exists, don't accept unauthenticated connection + if (anyAdmin && policies.LockSubscription && !isAuth) + return Unauthorized(); + + // Even if subscription are unlocked, it is forbidden to create admin unauthenticated + if (anyAdmin && request.IsAdministrator is true && !isAuth) + return Forbid(AuthenticationSchemes.ApiKey); + // You are de-facto admin if there is no other admin, else you need to be auth and pass policy requirements + bool isAdmin = anyAdmin ? (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanModifyServerSettings.Key))).Succeeded + && isAuth + : true; + // You need to be admin to create an admin + if (request.IsAdministrator is true && !isAdmin) + { + return Forbid(AuthenticationSchemes.ApiKey); + } + + if (!isAdmin && policies.LockSubscription) + { + // If we are not admin and subscriptions are locked, we need to check the Policies.CanCreateUser.Key permission + if (!isAuth || !(await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanCreateUser.Key))).Succeeded) + return Forbid(AuthenticationSchemes.ApiKey); + } + var user = new ApplicationUser { UserName = request.Email, Email = request.Email, - RequiresEmailConfirmation = policies.RequiresConfirmedEmail, - EmailConfirmed = request.EmailConfirmed.GetValueOrDefault(false) + RequiresEmailConfirmation = policies.RequiresConfirmedEmail }; var passwordValidation = await this._passwordValidator.ValidateAsync(_userManager, user, request.Password); if (!passwordValidation.Succeeded) @@ -79,6 +112,11 @@ namespace BTCPayServer.Controllers.RestApi.Users } return BadRequest(new ValidationProblemDetails(ModelState)); } + if (!isAdmin) + { + if (!await _throttleService.Throttle(ZoneLimits.Register, this.HttpContext.Connection.RemoteIpAddress, cancellationToken)) + return new TooManyRequestsResult(ZoneLimits.Register); + } var identityResult = await _userManager.CreateAsync(user, request.Password); if (!identityResult.Succeeded) { @@ -88,13 +126,23 @@ namespace BTCPayServer.Controllers.RestApi.Users } return BadRequest(new ValidationProblemDetails(ModelState)); } - else if (admin) + + if (request.IsAdministrator is true) { - await _roleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin)); + if (!anyAdmin) + { + await _roleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin)); + } await _userManager.AddToRoleAsync(user, Roles.ServerAdmin); + if (!anyAdmin) + { + // automatically lock subscriptions now that we have our first admin + policies.LockSubscription = true; + await _settingsRepository.UpdateSetting(policies); + } } - _eventAggregator.Publish(new UserRegisteredEvent() {Request = Request, User = user, Admin = admin}); - return CreatedAtAction("", user); + _eventAggregator.Publish(new UserRegisteredEvent() {Request = Request, User = user, Admin = request.IsAdministrator is true }); + return CreatedAtAction(string.Empty, user); } private ValidationProblemDetails CreateValidationProblem(string propertyName, string errorMessage) diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index ba845c9ea..07d1282e7 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -256,10 +256,12 @@ namespace BTCPayServer.Hosting if (btcPayEnv.IsDevelopping) { rateLimits.SetZone($"zone={ZoneLimits.Login} rate=1000r/min burst=100 nodelay"); + rateLimits.SetZone($"zone={ZoneLimits.Register} rate=1000r/min burst=100 nodelay"); } else { rateLimits.SetZone($"zone={ZoneLimits.Login} rate=5r/min burst=3 nodelay"); + rateLimits.SetZone($"zone={ZoneLimits.Register} rate=2r/min burst=2 nodelay"); } return rateLimits; }); diff --git a/BTCPayServer/ZoneLimits.cs b/BTCPayServer/ZoneLimits.cs index 8214330a5..08e722cec 100644 --- a/BTCPayServer/ZoneLimits.cs +++ b/BTCPayServer/ZoneLimits.cs @@ -8,5 +8,6 @@ namespace BTCPayServer public class ZoneLimits { public const string Login = "btcpaylogin"; + public const string Register = "btcpayregister"; } } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json index c421e63a8..db8b1ce93 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json @@ -24,7 +24,7 @@ "parameters": [ { "name": "permissions", - "description": "The permissions to request. Current permissions available: ServerManagement, StoreManagement", + "description": "The permissions to request. Current permissions available: ServerManagement, StoreManagement, ProfileManagement", "in": "query", "style": "form", "explode": true, @@ -111,7 +111,7 @@ "security": [ { "APIKey": [ - "btcpay.store.canmodifyprofile" + "ProfileManagement" ] } ] @@ -123,8 +123,7 @@ "Users" ], "summary": "Create user", - "description": "Create a new user", - "operationId": "Users_CreateUser", + "description": "Create a new user. This operation can be called without authentication if there is not any administrator yet on the server.", "requestBody": { "x-name": "request", "content": { @@ -135,19 +134,15 @@ "properties": { "email": { "type": "string", - "nullable": true + "nullable": false }, "password": { - "type": "string", - "nullable": true + "type": "string" }, "isAdministrator": { "type": "boolean", - "nullable": true - }, - "emailConfirmed": { - "type": "boolean", - "nullable": true + "nullable": true, + "default": false } } } @@ -176,13 +171,23 @@ } } } + }, + "401": { + "description": "If you need to authenticate for this endpoint (ie. the server settings policies lock subscriptions and that an admin already exists)" + }, + "403": { + "description": "If you are authenticated but forbidden to create a new user (ie. you don't have the ServerManagement permission)" + }, + "429": { + "description": "DDoS protection if you are creating more than 2 accounts every minutes (non-admin only)" } }, "security": [ { "APIKey": [ - "btcpay.store.cancreateuser" - ] + "ServerManagement" + ], + "Anonymous": [ ] } ] } From 0a0d8d53a47b17472c8e3461e6a5ed0ceaa379f1 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Wed, 18 Mar 2020 23:30:27 +0900 Subject: [PATCH 283/810] Improve redoc --- .../wwwroot/swagger/v1/swagger.template.json | 65 +++---------------- 1 file changed, 10 insertions(+), 55 deletions(-) diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json index db8b1ce93..84067ab08 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json @@ -24,7 +24,7 @@ "parameters": [ { "name": "permissions", - "description": "The permissions to request. Current permissions available: ServerManagement, StoreManagement, ProfileManagement", + "description": "The permissions to request. (See APIKey authentication)", "in": "query", "style": "form", "explode": true, @@ -243,58 +243,6 @@ } ] } - }, - "/api/v1/users/me/api-keys/current": { - "get": { - "tags": [ - "API Keys" - ], - "summary": "Get current API Key information", - "description": "View information about the current API key", - "operationId": "ApiKeys_GetKey2", - "responses": { - "200": { - "description": "Information about the current api key", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiKeyData" - } - } - } - } - }, - "security": [ - { - "APIKey": [] - } - ] - }, - "delete": { - "tags": [ - "API Keys" - ], - "summary": "Revoke the current API Key", - "description": "Revoke the current API key so that it cannot be used anymore", - "operationId": "ApiKeys_RevokeKey2", - "responses": { - "200": { - "description": "The key was revoked and is no longer usable", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiKeyData" - } - } - } - } - }, - "security": [ - { - "APIKey": [] - } - ] - } } }, "components": { @@ -403,15 +351,22 @@ "securitySchemes": { "APIKey": { "type": "apiKey", - "description": "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: token {token}. For a smoother experience, you can generate a url that redirects users to an API key creation screen.", + "description": "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: token {token}. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n You can create API Keys in the user profile / Api Keys. (Available are: ServerManagement, StoreManagement, ProfileManagement)", "name": "Authorization", "in": "header" + }, + "Anonymous": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "Nothing required", + "description": "Anonymous authentication" } } }, "security": [ { - "APIKey": [] + "APIKey": [], + "Anonymous": [] } ], "tags": [ From 65d26ad8a11f837b56624ac228aad1cb35dea4a1 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Wed, 18 Mar 2020 23:35:17 +0900 Subject: [PATCH 284/810] improve redoc documentation --- .../wwwroot/swagger/v1/swagger.template.json | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json index 84067ab08..7f06f6af1 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json @@ -134,13 +134,16 @@ "properties": { "email": { "type": "string", + "description": "The email of the new user", "nullable": false }, "password": { - "type": "string" + "type": "string", + "description": "The password of the new user" }, "isAdministrator": { "type": "boolean", + "description": "Make this user administrator (only if your APIKey has ServerManagment permission)", "nullable": true, "default": false } @@ -253,17 +256,21 @@ "properties": { "id": { "type": "string", - "nullable": true + "description": "The id of the new user", + "nullable": false }, "email": { "type": "string", - "nullable": true + "description": "The email of the new user", + "nullable": false }, "emailConfirmed": { - "type": "boolean" + "type": "boolean", + "description": "True if the email has been confirmed by the user" }, "requiresEmailConfirmation": { - "type": "boolean" + "type": "boolean", + "description": "True if the email requires email confirmation to log in" } } }, From 47c1164003dc7c1cde8bc4ca3ee696bdd53269bc Mon Sep 17 00:00:00 2001 From: rockstardev Date: Wed, 18 Mar 2020 18:17:21 -0500 Subject: [PATCH 285/810] Adding logging to detect failure of CanManageWallet test If all logs are present it's possibly issue with TempData --- BTCPayServer.Tests/SeleniumTester.cs | 1 - .../Controllers/StoresController.BTCLike.cs | 15 +++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index 5799cc769..5a9425e2c 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -126,7 +126,6 @@ namespace BTCPayServer.Tests Driver.FindElement(By.Id($"Modify{cryptoCode}")).ForceClick(); Driver.FindElement(By.Id("import-from-btn")).ForceClick(); Driver.FindElement(By.Id("nbxplorergeneratewalletbtn")).ForceClick(); - Thread.Sleep(200); // allow for modal to fade in Driver.WaitForElement(By.Id("ExistingMnemonic")).SendKeys(seed); SetCheckbox(Driver.FindElement(By.Id("SavePrivateKeys")), privkeys); SetCheckbox(Driver.FindElement(By.Id("ImportKeysToRPC")), importkeys); diff --git a/BTCPayServer/Controllers/StoresController.BTCLike.cs b/BTCPayServer/Controllers/StoresController.BTCLike.cs index 1926cecb1..8b33bd5bf 100644 --- a/BTCPayServer/Controllers/StoresController.BTCLike.cs +++ b/BTCPayServer/Controllers/StoresController.BTCLike.cs @@ -22,6 +22,8 @@ using NBXplorer.DerivationStrategy; using NBXplorer.Models; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using BTCPayServer.Logging; +using Microsoft.Extensions.Logging; namespace BTCPayServer.Controllers { @@ -328,11 +330,15 @@ namespace BTCPayServer.Controllers public async Task GenerateNBXWallet(string storeId, string cryptoCode, GenerateWalletRequest request) { + Logs.Events.LogInformation($"GenerateNBXWallet called {storeId}, {cryptoCode}, {request.ToJson()}"); + if (!await CanUseHotWallet()) { return NotFound(); } - + + Logs.Events.LogInformation($"GenerateNBXWallet after CanUseHotWallet"); + var network = _NetworkProvider.GetNetwork(cryptoCode); var client = _ExplorerProvider.GetExplorerClient(cryptoCode); var response = await client.GenerateWalletAsync(request); @@ -345,6 +351,9 @@ namespace BTCPayServer.Controllers }); return RedirectToAction("AddDerivationScheme", new {storeId, cryptoCode}); } + + Logs.Events.LogInformation($"GenerateNBXWallet after GenerateWalletAsync"); + var store = HttpContext.GetStoreData(); var result = await AddDerivationScheme(storeId, new DerivationSchemeViewModel() @@ -362,7 +371,7 @@ namespace BTCPayServer.Controllers Enabled = !store.GetStoreBlob() .IsExcluded(new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike)) }, cryptoCode); - + TempData.SetStatusMessageModel(new StatusMessageModel() { Severity = StatusMessageModel.StatusSeverity.Success, @@ -370,6 +379,8 @@ namespace BTCPayServer.Controllers ? "Your wallet has been imported." : $"Your wallet has been generated. Please store your seed securely!
    {response.Mnemonic}" }); + + Logs.Events.LogInformation($"GenerateNBXWallet returning success result"); return result; } From 0a8abaf7d5c89d6012c750a8c30ae06ed73d7cb1 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Wed, 18 Mar 2020 18:40:21 -0500 Subject: [PATCH 286/810] Refactoring if condition to ensure CanCreateUser permission Fixing UsersControllerTests --- BTCPayServer/Controllers/RestApi/Users/UsersController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs index 35493547d..f1589465a 100644 --- a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs +++ b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs @@ -90,11 +90,11 @@ namespace BTCPayServer.Controllers.RestApi.Users return Forbid(AuthenticationSchemes.ApiKey); } - if (!isAdmin && policies.LockSubscription) + // check if we have permission to create users + var canCreateUser = (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanCreateUser.Key))).Succeeded; + if (!canCreateUser) { - // If we are not admin and subscriptions are locked, we need to check the Policies.CanCreateUser.Key permission - if (!isAuth || !(await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanCreateUser.Key))).Succeeded) - return Forbid(AuthenticationSchemes.ApiKey); + return Forbid(AuthenticationSchemes.ApiKey); } var user = new ApplicationUser From ccfca65c417a6733379aa4dea2279cd5f0e86be3 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Wed, 18 Mar 2020 18:55:45 -0500 Subject: [PATCH 287/810] Reverting changes to UsersController because of CanCreateUsersViaAPI test --- BTCPayServer/Controllers/RestApi/Users/UsersController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs index f1589465a..35493547d 100644 --- a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs +++ b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs @@ -90,11 +90,11 @@ namespace BTCPayServer.Controllers.RestApi.Users return Forbid(AuthenticationSchemes.ApiKey); } - // check if we have permission to create users - var canCreateUser = (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanCreateUser.Key))).Succeeded; - if (!canCreateUser) + if (!isAdmin && policies.LockSubscription) { - return Forbid(AuthenticationSchemes.ApiKey); + // If we are not admin and subscriptions are locked, we need to check the Policies.CanCreateUser.Key permission + if (!isAuth || !(await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanCreateUser.Key))).Succeeded) + return Forbid(AuthenticationSchemes.ApiKey); } var user = new ApplicationUser From 8432cd5477534a5710050024e0734728d95d640f Mon Sep 17 00:00:00 2001 From: rockstardev Date: Wed, 18 Mar 2020 19:01:27 -0500 Subject: [PATCH 288/810] Adding new check for expected behavior in UsersControllerTests --- .../Controllers/RestApi/Users/UsersController.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs index 35493547d..6c4aa7e13 100644 --- a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs +++ b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs @@ -86,17 +86,20 @@ namespace BTCPayServer.Controllers.RestApi.Users : true; // You need to be admin to create an admin if (request.IsAdministrator is true && !isAdmin) - { return Forbid(AuthenticationSchemes.ApiKey); - } + var canCreateUser = (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanCreateUser.Key))).Succeeded; if (!isAdmin && policies.LockSubscription) { // If we are not admin and subscriptions are locked, we need to check the Policies.CanCreateUser.Key permission - if (!isAuth || !(await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanCreateUser.Key))).Succeeded) + if (!isAuth || !canCreateUser) return Forbid(AuthenticationSchemes.ApiKey); } + // Forbid non-admin users without CanCreateUser permission to create accounts + if (isAuth && !isAdmin && !canCreateUser) + return Forbid(AuthenticationSchemes.ApiKey); + var user = new ApplicationUser { UserName = request.Email, From 967b02e3735ec8950bd509d62a3d46890191a5aa Mon Sep 17 00:00:00 2001 From: rockstardev Date: Wed, 18 Mar 2020 19:17:15 -0500 Subject: [PATCH 289/810] Commenting out conflicting check --- BTCPayServer.Tests/GreenfieldAPITests.cs | 12 ++++++------ .../Controllers/RestApi/Users/UsersController.cs | 5 +++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 1e7403fd9..8fe50674c 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -152,12 +152,12 @@ namespace BTCPayServer.Tests await Assert.ThrowsAsync(async () => await clientInsufficient.GetCurrentUser()); await clientServer.GetCurrentUser(); - - await Assert.ThrowsAsync(async () => await clientInsufficient.CreateUser(new CreateApplicationUserRequest() - { - Email = $"{Guid.NewGuid()}@g.com", - Password = Guid.NewGuid().ToString() - })); + // TODO: Disabling this check for now because it conflicts with expecation in line 120 + //await Assert.ThrowsAsync(async () => await clientInsufficient.CreateUser(new CreateApplicationUserRequest() + //{ + // Email = $"{Guid.NewGuid()}@g.com", + // Password = Guid.NewGuid().ToString() + //})); var newUser = await clientServer.CreateUser(new CreateApplicationUserRequest() { diff --git a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs index 6c4aa7e13..8bd4e26fb 100644 --- a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs +++ b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs @@ -96,9 +96,10 @@ namespace BTCPayServer.Controllers.RestApi.Users return Forbid(AuthenticationSchemes.ApiKey); } + // TODO: Check if needed to reenable // Forbid non-admin users without CanCreateUser permission to create accounts - if (isAuth && !isAdmin && !canCreateUser) - return Forbid(AuthenticationSchemes.ApiKey); + //if (isAuth && !isAdmin && !canCreateUser) + // return Forbid(AuthenticationSchemes.ApiKey); var user = new ApplicationUser { From 2105b44610025de042bdce7af0f900c89010a9f4 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Thu, 19 Mar 2020 13:30:53 +0900 Subject: [PATCH 290/810] Make sure the create user is respecting the disable-registration settings --- BTCPayServer.Tests/BTCPayServerTester.cs | 8 +++++--- BTCPayServer.Tests/GreenfieldAPITests.cs | 15 ++++++++------- .../Controllers/RestApi/Users/UsersController.cs | 12 +++++++++--- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index a9924cde4..03aa4ef5d 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -95,7 +95,8 @@ namespace BTCPayServer.Tests public HashSet Chains { get; set; } = new HashSet(){"BTC"}; public bool UseLightning { get; set; } - + public bool AllowAdminRegistration { get; set; } = true; + public bool DisableRegistration { get; set; } = false; public async Task StartAsync() { if (!Directory.Exists(_Directory)) @@ -137,7 +138,8 @@ namespace BTCPayServer.Tests config.AppendLine($"lbtc.explorer.url={LBTCNBXplorerUri.AbsoluteUri}"); config.AppendLine($"lbtc.explorer.cookiefile=0"); } - config.AppendLine("allow-admin-registration=1"); + if (AllowAdminRegistration) + config.AppendLine("allow-admin-registration=1"); config.AppendLine($"torrcfile={TestUtils.GetTestDataFullPath("Tor/torrc")}"); config.AppendLine($"debuglog=debug.log"); @@ -161,7 +163,7 @@ namespace BTCPayServer.Tests HttpClient = new HttpClient(); HttpClient.BaseAddress = ServerUri; Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); - var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath, "--disable-registration", "false" }); + var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath, "--disable-registration", DisableRegistration ? "true" : "false" }); _Host = new WebHostBuilder() .UseConfiguration(conf) .UseContentRoot(FindBTCPayServerDirectory()) diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 8fe50674c..4d3ebb483 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -61,6 +61,7 @@ namespace BTCPayServer.Tests { using (var tester = ServerTester.Create(newDb: true)) { + tester.PayTester.DisableRegistration = true; await tester.StartAsync(); var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri); await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest())); @@ -133,8 +134,9 @@ namespace BTCPayServer.Tests [Trait("Integration", "Integration")] public async Task UsersControllerTests() { - using (var tester = ServerTester.Create()) + using (var tester = ServerTester.Create(newDb: true)) { + tester.PayTester.DisableRegistration = true; await tester.StartAsync(); var user = tester.NewAccount(); user.GrantAccess(); @@ -152,12 +154,11 @@ namespace BTCPayServer.Tests await Assert.ThrowsAsync(async () => await clientInsufficient.GetCurrentUser()); await clientServer.GetCurrentUser(); - // TODO: Disabling this check for now because it conflicts with expecation in line 120 - //await Assert.ThrowsAsync(async () => await clientInsufficient.CreateUser(new CreateApplicationUserRequest() - //{ - // Email = $"{Guid.NewGuid()}@g.com", - // Password = Guid.NewGuid().ToString() - //})); + await Assert.ThrowsAsync(async () => await clientInsufficient.CreateUser(new CreateApplicationUserRequest() + { + Email = $"{Guid.NewGuid()}@g.com", + Password = Guid.NewGuid().ToString() + })); var newUser = await clientServer.CreateUser(new CreateApplicationUserRequest() { diff --git a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs index 8bd4e26fb..dd5894815 100644 --- a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs +++ b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs @@ -30,6 +30,7 @@ namespace BTCPayServer.Controllers.RestApi.Users private readonly EventAggregator _eventAggregator; private readonly IPasswordValidator _passwordValidator; private readonly RateLimitService _throttleService; + private readonly BTCPayServerOptions _options; private readonly IAuthorizationService _authorizationService; public UsersController(UserManager userManager, BTCPayServerOptions btcPayServerOptions, @@ -37,6 +38,7 @@ namespace BTCPayServer.Controllers.RestApi.Users EventAggregator eventAggregator, IPasswordValidator passwordValidator, NicolasDorier.RateLimits.RateLimitService throttleService, + Configuration.BTCPayServerOptions options, IAuthorizationService authorizationService) { _userManager = userManager; @@ -46,6 +48,7 @@ namespace BTCPayServer.Controllers.RestApi.Users _eventAggregator = eventAggregator; _passwordValidator = passwordValidator; _throttleService = throttleService; + _options = options; _authorizationService = authorizationService; } @@ -140,9 +143,12 @@ namespace BTCPayServer.Controllers.RestApi.Users await _userManager.AddToRoleAsync(user, Roles.ServerAdmin); if (!anyAdmin) { - // automatically lock subscriptions now that we have our first admin - policies.LockSubscription = true; - await _settingsRepository.UpdateSetting(policies); + if (_options.DisableRegistration) + { + // automatically lock subscriptions now that we have our first admin + policies.LockSubscription = true; + await _settingsRepository.UpdateSetting(policies); + } } } _eventAggregator.Publish(new UserRegisteredEvent() {Request = Request, User = user, Admin = request.IsAdministrator is true }); From eac33d494afcfdec93c039ba4b4e94b3fe678599 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Thu, 19 Mar 2020 13:34:11 +0900 Subject: [PATCH 291/810] Add logs --- BTCPayServer/Controllers/AccountController.cs | 1 + BTCPayServer/Controllers/RestApi/Users/UsersController.cs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/BTCPayServer/Controllers/AccountController.cs b/BTCPayServer/Controllers/AccountController.cs index 74b345832..118e83ab1 100644 --- a/BTCPayServer/Controllers/AccountController.cs +++ b/BTCPayServer/Controllers/AccountController.cs @@ -454,6 +454,7 @@ namespace BTCPayServer.Controllers if (_Options.DisableRegistration) { // Once the admin user has been created lock subsequent user registrations (needs to be disabled for unit tests that require multiple users). + Logs.PayServer.LogInformation("First admin created, disabling subscription (disable-registration is set to true)"); policies.LockSubscription = true; await _SettingsRepository.UpdateSetting(policies); } diff --git a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs index dd5894815..0564075d9 100644 --- a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs +++ b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.Extensions.Logging; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; @@ -7,6 +8,7 @@ using BTCPayServer.Client.Models; using BTCPayServer.Configuration; using BTCPayServer.Data; using BTCPayServer.Events; +using BTCPayServer.Logging; using BTCPayServer.Security; using BTCPayServer.Security.APIKeys; using BTCPayServer.Services; @@ -146,6 +148,7 @@ namespace BTCPayServer.Controllers.RestApi.Users if (_options.DisableRegistration) { // automatically lock subscriptions now that we have our first admin + Logs.PayServer.LogInformation("First admin created, disabling subscription (disable-registration is set to true)"); policies.LockSubscription = true; await _settingsRepository.UpdateSetting(policies); } From d6c66d0c038294077fc3f7de6b888df0aeab1d64 Mon Sep 17 00:00:00 2001 From: Kukks Date: Thu, 19 Mar 2020 09:44:47 +0100 Subject: [PATCH 292/810] New feature: Coin Selection This opt-in feature allows you to select which utxos you want to use for a specific transaction. --- .../Controllers/WalletsController.PSBT.cs | 4 + BTCPayServer/Controllers/WalletsController.cs | 31 ++- .../WalletViewModels/WalletSendModel.cs | 14 ++ BTCPayServer/Views/Shared/_Layout.cshtml | 12 ++ .../Views/Wallets/CoinSelection.cshtml | 182 ++++++++++++++++++ BTCPayServer/Views/Wallets/WalletSend.cshtml | 19 +- BTCPayServer/bundleconfig.json | 9 + BTCPayServer/wwwroot/js/WalletSend.js | 2 +- BTCPayServer/wwwroot/main/site.css | 11 +- BTCPayServer/wwwroot/main/site.js | 3 - 10 files changed, 279 insertions(+), 8 deletions(-) create mode 100644 BTCPayServer/Views/Wallets/CoinSelection.cshtml diff --git a/BTCPayServer/Controllers/WalletsController.PSBT.cs b/BTCPayServer/Controllers/WalletsController.PSBT.cs index d4cab9b3a..781488129 100644 --- a/BTCPayServer/Controllers/WalletsController.PSBT.cs +++ b/BTCPayServer/Controllers/WalletsController.PSBT.cs @@ -20,6 +20,10 @@ namespace BTCPayServer.Controllers { var nbx = ExplorerClientProvider.GetExplorerClient(network); CreatePSBTRequest psbtRequest = new CreatePSBTRequest(); + if (sendModel.InputSelection) + { + psbtRequest.IncludeOnlyOutpoints = sendModel.SelectedInputs?.Select(OutPoint.Parse)?.ToList()?? new List(); + } foreach (var transactionOutput in sendModel.Outputs) { var psbtDestination = new CreatePSBTDestination(); diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index e4dc74e6d..1c4ca1580 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -463,7 +463,36 @@ namespace BTCPayServer.Controllers } decimal transactionAmountSum = 0; - + if (command == "toggle-input-selection") + { + vm.InputSelection = !vm.InputSelection; + } + if (vm.InputSelection) + { + var schemeSettings = GetDerivationSchemeSettings(walletId); + var walletBlobAsync = await WalletRepository.GetWalletInfo(walletId); + var walletTransactionsInfoAsync = await WalletRepository.GetWalletTransactionsInfo(walletId); + + var utxos = await _walletProvider.GetWallet(network).GetUnspentCoins(schemeSettings.AccountDerivation, cancellation); + vm.InputsAvailable = utxos.Select(coin => + { + walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info); + return new WalletSendModel.InputSelectionOption() + { + Outpoint = coin.OutPoint.ToString(), + Amount = coin.Value.GetValue(network), + Comment = info?.Comment, + Labels = info == null? null :walletBlobAsync.GetLabels(info), + Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, coin.OutPoint.Hash.ToString()) + }; + }).ToArray(); + } + + if (command == "toggle-input-selection") + { + ModelState.Clear(); + return View(vm); + } if (command == "add-output") { ModelState.Clear(); diff --git a/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs b/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs index 73c72e121..55ec9ca4f 100644 --- a/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs +++ b/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Data; +using NBXplorer.Models; namespace BTCPayServer.Models.WalletViewModels { @@ -47,5 +49,17 @@ namespace BTCPayServer.Models.WalletViewModels public bool DisableRBF { get; set; } public bool NBXSeedAvailable { get; set; } + public bool InputSelection { get; set; } + public InputSelectionOption[] InputsAvailable { get; set; } + public IEnumerable SelectedInputs { get; set; } + + public class InputSelectionOption + { + public IEnumerable
    + +
    + + + diff --git a/BTCPayServer/Views/Wallets/WalletSend.cshtml b/BTCPayServer/Views/Wallets/WalletSend.cshtml index fbfe61dd0..667422e63 100644 --- a/BTCPayServer/Views/Wallets/WalletSend.cshtml +++ b/BTCPayServer/Views/Wallets/WalletSend.cshtml @@ -14,8 +14,9 @@
    }
    -
    +
    + @@ -34,6 +35,19 @@ } } + + @if (Model.InputSelection) + { + + + + } + @if (Model.Outputs.Count == 1) {
    @@ -150,6 +164,9 @@
    } +
    + +
    diff --git a/BTCPayServer/bundleconfig.json b/BTCPayServer/bundleconfig.json index e71cd9c6f..767a0d9c3 100644 --- a/BTCPayServer/bundleconfig.json +++ b/BTCPayServer/bundleconfig.json @@ -179,5 +179,14 @@ "wwwroot/vendor/bootstrap-vue/bootstrap-vue.css", "wwwroot/payment-request/**/*.css" ] + }, + { + "outputFileName": "wwwroot/bundles/wallet-coin-selection-bundle.min.js", + "inputFiles": [ + "wwwroot/vendor/vuejs/vue.min.js", + "wwwroot/vendor/babel-polyfill/polyfill.min.js", + ] } + + ] diff --git a/BTCPayServer/wwwroot/js/WalletSend.js b/BTCPayServer/wwwroot/js/WalletSend.js index 3da3deea2..c939e63e7 100644 --- a/BTCPayServer/wwwroot/js/WalletSend.js +++ b/BTCPayServer/wwwroot/js/WalletSend.js @@ -35,7 +35,7 @@ $(function () { $(".crypto-balance-link").on("click", function (elem) { var val = $(this).text(); - var parentContainer = $(this).parents(".transaction-output-form"); + var parentContainer = $(this).parents(".form-group"); var outputAmountElement = parentContainer.find(".output-amount"); outputAmountElement.val(val); parentContainer.find(".subtract-fees").prop('checked', true); diff --git a/BTCPayServer/wwwroot/main/site.css b/BTCPayServer/wwwroot/main/site.css index 928d2cabe..23c8ad5b6 100644 --- a/BTCPayServer/wwwroot/main/site.css +++ b/BTCPayServer/wwwroot/main/site.css @@ -7,11 +7,13 @@ html { .logo { height: 45px; } - -.only-for-js, +.hide-when-js, .input-group-clear { display: none; } +.only-for-js { + display: block; +} .wraptextAuto { max-width: 300px; @@ -142,3 +144,8 @@ pre { .pin-button:hover { background-color: lightgray; } +[v-cloak] > * { display:none } +[v-cloak]::before { content: "loading…" } +.cursor-pointer{ + cursor: pointer; +} diff --git a/BTCPayServer/wwwroot/main/site.js b/BTCPayServer/wwwroot/main/site.js index 25af2dab6..5870c39ec 100644 --- a/BTCPayServer/wwwroot/main/site.js +++ b/BTCPayServer/wwwroot/main/site.js @@ -49,9 +49,6 @@ }); }); - - $(".only-for-js").show(); - function handleInputGroupClearButtonDisplay(element) { var inputs = $(element).parents(".input-group").find("input"); From 8f18be727bc72e8ea2bf05015201c3912ad31847 Mon Sep 17 00:00:00 2001 From: Kukks Date: Thu, 19 Mar 2020 10:02:31 +0100 Subject: [PATCH 293/810] add clear option --- BTCPayServer/Views/Wallets/CoinSelection.cshtml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/BTCPayServer/Views/Wallets/CoinSelection.cshtml b/BTCPayServer/Views/Wallets/CoinSelection.cshtml index 142b562cb..ad1ddbf8a 100644 --- a/BTCPayServer/Views/Wallets/CoinSelection.cshtml +++ b/BTCPayServer/Views/Wallets/CoinSelection.cshtml @@ -11,7 +11,14 @@
  • - +
    + +
    + +
    +
  • Date: Thu, 19 Mar 2020 10:08:33 +0100 Subject: [PATCH 294/810] make no js version a littler better --- .../Models/WalletViewModels/WalletSendModel.cs | 2 ++ BTCPayServer/Views/Shared/_Layout.cshtml | 1 + BTCPayServer/Views/Wallets/WalletSend.cshtml | 16 +++++++++------- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs b/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs index 55ec9ca4f..5cc12c361 100644 --- a/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs +++ b/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs @@ -51,6 +51,8 @@ namespace BTCPayServer.Models.WalletViewModels public bool NBXSeedAvailable { get; set; } public bool InputSelection { get; set; } public InputSelectionOption[] InputsAvailable { get; set; } + + [Display(Name = "UTXOs to spend from")] public IEnumerable SelectedInputs { get; set; } public class InputSelectionOption diff --git a/BTCPayServer/Views/Shared/_Layout.cshtml b/BTCPayServer/Views/Shared/_Layout.cshtml index def29b359..56544113f 100644 --- a/BTCPayServer/Views/Shared/_Layout.cshtml +++ b/BTCPayServer/Views/Shared/_Layout.cshtml @@ -23,6 +23,7 @@ .only-for-js{ display:none !important; } + [v-cloak]::before { content: "" !important; } diff --git a/BTCPayServer/Views/Wallets/WalletSend.cshtml b/BTCPayServer/Views/Wallets/WalletSend.cshtml index 667422e63..38b709fce 100644 --- a/BTCPayServer/Views/Wallets/WalletSend.cshtml +++ b/BTCPayServer/Views/Wallets/WalletSend.cshtml @@ -38,13 +38,15 @@ @if (Model.InputSelection) { - - +
    + + +
    } From 29a807696b8863f5ec109d05b845c81c8928cb71 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Thu, 19 Mar 2020 19:11:15 +0900 Subject: [PATCH 295/810] Refactor permissions of GreenField --- BTCPayServer.Client/Permissions.cs | 186 ++++++++++++++-- BTCPayServer.Data/Data/APIKeyData.cs | 7 - BTCPayServer.Tests/ApiKeysTests.cs | 125 ++++++----- BTCPayServer.Tests/GreenfieldAPITests.cs | 15 +- BTCPayServer.Tests/SeleniumTester.cs | 6 +- BTCPayServer.Tests/TestAccount.cs | 7 +- BTCPayServer.Tests/UnitTest1.cs | 17 ++ BTCPayServer/BTCPayServer.csproj | 2 +- .../Controllers/InvoiceController.API.cs | 3 +- .../Controllers/InvoiceController.UI.cs | 3 +- .../Controllers/ManageController.APIKeys.cs | 202 ++++++++++++------ .../RestApi/ApiKeys/ApiKeysController.cs | 4 +- .../RestApi/TestApiKeyController.cs | 25 +-- .../RestApi/Users/UsersController.cs | 12 +- BTCPayServer/Controllers/ServerController.cs | 3 +- .../Controllers/StoresController.BTCLike.cs | 3 +- BTCPayServer/Controllers/StoresController.cs | 3 +- BTCPayServer/Controllers/VaultController.cs | 3 +- BTCPayServer/Controllers/WalletsController.cs | 9 +- BTCPayServer/Extensions.cs | 9 + .../APIKeys/APIKeyAuthenticationHandler.cs | 6 +- .../APIKeys/APIKeyAuthorizationHandler.cs | 83 ++++--- .../Security/APIKeys/APIKeyConstants.cs | 15 +- .../Security/APIKeys/APIKeyExtensions.cs | 35 ++- .../Bitpay/BitpayAuthorizationHandler.cs | 3 +- .../Security/CookieAuthorizationHandler.cs | 7 +- BTCPayServer/Security/Policies.cs | 41 +--- .../Monero/UI/MoneroLikeStoreController.cs | 5 +- BTCPayServer/Views/Manage/APIKeys.cshtml | 3 +- BTCPayServer/Views/Manage/AddApiKey.cshtml | 54 ++--- .../Views/Manage/AuthorizeAPIKey.cshtml | 89 +++----- 31 files changed, 581 insertions(+), 404 deletions(-) diff --git a/BTCPayServer.Client/Permissions.cs b/BTCPayServer.Client/Permissions.cs index ef26d83f5..de225cfc2 100644 --- a/BTCPayServer.Client/Permissions.cs +++ b/BTCPayServer.Client/Permissions.cs @@ -5,25 +5,183 @@ using System.Text.Json.Serialization; namespace BTCPayServer.Client { - public static class Permissions + public class Permission { - public const string ServerManagement = nameof(ServerManagement); - public const string StoreManagement = nameof(StoreManagement); - public const string ProfileManagement = nameof(ProfileManagement); + public const string CanModifyServerSettings = "btcpay.server.canmodifyserversettings"; + public const string CanModifyStoreSettings = "btcpay.store.canmodifystoresettings"; + public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings"; + public const string CanCreateInvoice = "btcpay.store.cancreateinvoice"; + public const string CanModifyProfile = "btcpay.user.canmodifyprofile"; + public const string CanViewProfile = "btcpay.user.canviewprofile"; + public const string CanCreateUser = "btcpay.server.cancreateuser"; + public const string Unrestricted = "unrestricted"; - public static string[] GetAllPermissionKeys() + public static IEnumerable AllPolicies { - return new[] + get { - ServerManagement, - StoreManagement, - ProfileManagement - }; + yield return CanCreateInvoice; + yield return CanModifyServerSettings; + yield return CanModifyStoreSettings; + yield return CanViewStoreSettings; + yield return CanModifyProfile; + yield return CanViewProfile; + yield return CanCreateUser; + yield return Unrestricted; + } } - public static string GetStorePermission(string storeId) => $"{nameof(StoreManagement)}:{storeId}"; - public static IEnumerable ExtractStorePermissionsIds(IEnumerable permissions) => permissions - .Where(s => s.StartsWith($"{nameof(StoreManagement)}:", StringComparison.InvariantCulture)) - .Select(s => s.Split(":")[1]); + public static Permission Create(string policy, string storeId = null) + { + if (TryCreatePermission(policy, storeId, out var r)) + return r; + throw new ArgumentException("Invalid Permission"); + } + + public static bool TryCreatePermission(string policy, string storeId, out Permission permission) + { + permission = null; + if (policy == null) + throw new ArgumentNullException(nameof(policy)); + policy = policy.Trim().ToLowerInvariant(); + if (!IsValidPolicy(policy)) + return false; + if (storeId != null && !IsStorePolicy(policy)) + return false; + permission = new Permission(policy, storeId); + return true; + } + + public static bool TryParse(string str, out Permission permission) + { + permission = null; + if (str == null) + throw new ArgumentNullException(nameof(str)); + str = str.Trim(); + var separator = str.IndexOf(':'); + if (separator == -1) + { + str = str.ToLowerInvariant(); + if (!IsValidPolicy(str)) + return false; + permission = new Permission(str, null); + return true; + } + else + { + var policy = str.Substring(0, separator).ToLowerInvariant(); + if (!IsValidPolicy(policy)) + return false; + if (!IsStorePolicy(policy)) + return false; + var storeId = str.Substring(separator + 1); + if (storeId.Length == 0) + return false; + permission = new Permission(policy, storeId); + return true; + } + } + + private static bool IsValidPolicy(string policy) + { + return AllPolicies.Any(p => p.Equals(policy, StringComparison.OrdinalIgnoreCase)); + } + + private static bool IsStorePolicy(string policy) + { + return policy.StartsWith("btcpay.store", StringComparison.OrdinalIgnoreCase); + } + + internal Permission(string policy, string storeId) + { + Policy = policy; + StoreId = storeId; + } + + public bool Contains(Permission subpermission) + { + if (subpermission is null) + throw new ArgumentNullException(nameof(subpermission)); + + if (!ContainsPolicy(subpermission.Policy)) + { + return false; + } + if (!IsStorePolicy(subpermission.Policy)) + return true; + return StoreId == null || subpermission.StoreId == this.StoreId; + } + + public static IEnumerable ToPermissions(string[] permissions) + { + if (permissions == null) + throw new ArgumentNullException(nameof(permissions)); + foreach (var p in permissions) + { + if (TryParse(p, out var pp)) + yield return pp; + } + } + public static IEnumerable ToPermissions(string permissionsFormatted) + { + foreach(var part in permissionsFormatted.Split(';', StringSplitOptions.RemoveEmptyEntries)) + { + if (Permission.TryParse(part, out var p)) + yield return p; + } + } + + private bool ContainsPolicy(string subpolicy) + { + if (this.Policy == Unrestricted) + return true; + if (this.Policy == subpolicy) + return true; + if (subpolicy == CanViewStoreSettings && this.Policy == CanModifyStoreSettings) + return true; + if (subpolicy == CanCreateInvoice && this.Policy == CanModifyStoreSettings) + return true; + if (subpolicy == CanViewProfile && this.Policy == CanModifyProfile) + return true; + return false; + } + + public string StoreId { get; } + public string Policy { get; } + + public override string ToString() + { + if (StoreId != null) + { + return $"{Policy}:{StoreId}"; + } + return Policy; + } + + public override bool Equals(object obj) + { + Permission item = obj as Permission; + if (item == null) + return false; + return ToString().Equals(item.ToString()); + } + public static bool operator ==(Permission a, Permission b) + { + if (System.Object.ReferenceEquals(a, b)) + return true; + if (((object)a == null) || ((object)b == null)) + return false; + return a.ToString() == b.ToString(); + } + + public static bool operator !=(Permission a, Permission b) + { + return !(a == b); + } + + public override int GetHashCode() + { + return ToString().GetHashCode(); + } } } diff --git a/BTCPayServer.Data/Data/APIKeyData.cs b/BTCPayServer.Data/Data/APIKeyData.cs index c538a9bd9..00722dcdd 100644 --- a/BTCPayServer.Data/Data/APIKeyData.cs +++ b/BTCPayServer.Data/Data/APIKeyData.cs @@ -27,13 +27,6 @@ namespace BTCPayServer.Data public StoreData StoreData { get; set; } public ApplicationUser User { get; set; } public string Label { get; set; } - public string[] GetPermissions() { return Permissions?.Split(';') ?? new string[0]; } - - public void SetPermissions(IEnumerable permissions) - { - Permissions = string.Join(';', - permissions?.Select(s => s.Replace(";", string.Empty)) ?? new string[0]); - } } public enum APIKeyType diff --git a/BTCPayServer.Tests/ApiKeysTests.cs b/BTCPayServer.Tests/ApiKeysTests.cs index 419fb2a60..e4917fa73 100644 --- a/BTCPayServer.Tests/ApiKeysTests.cs +++ b/BTCPayServer.Tests/ApiKeysTests.cs @@ -42,49 +42,46 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); user.GrantAccess(); - - await user.CreateStoreAsync(); + await user.MakeAdmin(false); s.GoToLogin(); s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password); s.GoToProfile(ManageNavPages.APIKeys); s.Driver.FindElement(By.Id("AddApiKey")).Click(); - if (!user.IsAdmin) - { - //not an admin, so this permission should not show - Assert.DoesNotContain("ServerManagementPermission", s.Driver.PageSource); - await user.MakeAdmin(); - s.Logout(); - s.GoToLogin(); - s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password); - s.GoToProfile(ManageNavPages.APIKeys); - s.Driver.FindElement(By.Id("AddApiKey")).Click(); - } + + //not an admin, so this permission should not show + Assert.DoesNotContain("btcpay.server.canmodifyserversettings", s.Driver.PageSource); + await user.MakeAdmin(); + s.Logout(); + s.GoToLogin(); + s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password); + s.GoToProfile(ManageNavPages.APIKeys); + s.Driver.FindElement(By.Id("AddApiKey")).Click(); + Assert.Contains("btcpay.server.canmodifyserversettings", s.Driver.PageSource); //server management should show now - s.SetCheckbox(s, "ServerManagementPermission", true); - s.SetCheckbox(s, "StoreManagementPermission", true); + s.SetCheckbox(s, "btcpay.server.canmodifyserversettings", true); + s.SetCheckbox(s, "btcpay.store.canmodifystoresettings", true); s.Driver.FindElement(By.Id("Generate")).Click(); var superApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; //this api key has access to everything - await TestApiAgainstAccessToken(superApiKey, tester, user, Permissions.ServerManagement, - Permissions.StoreManagement); + await TestApiAgainstAccessToken(superApiKey, tester, user, $"{Permission.CanModifyServerSettings};{Permission.CanModifyStoreSettings}"); s.Driver.FindElement(By.Id("AddApiKey")).Click(); - s.SetCheckbox(s, "ServerManagementPermission", true); + s.SetCheckbox(s, "btcpay.server.canmodifyserversettings", true); s.Driver.FindElement(By.Id("Generate")).Click(); var serverOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user, - Permissions.ServerManagement); + Permission.CanModifyServerSettings); s.Driver.FindElement(By.Id("AddApiKey")).Click(); - s.SetCheckbox(s, "StoreManagementPermission", true); + s.SetCheckbox(s, "btcpay.store.canmodifystoresettings", true); s.Driver.FindElement(By.Id("Generate")).Click(); var allStoreOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; await TestApiAgainstAccessToken(allStoreOnlyApiKey, tester, user, - Permissions.StoreManagement); + Permission.CanModifyStoreSettings); s.Driver.FindElement(By.Id("AddApiKey")).Click(); s.Driver.FindElement(By.CssSelector("button[value=change-store-mode]")).Click(); @@ -96,12 +93,12 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("Generate")).Click(); var selectiveStoreApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; await TestApiAgainstAccessToken(selectiveStoreApiKey, tester, user, - Permissions.GetStorePermission(storeId)); + Permission.Create(Permission.CanModifyStoreSettings, storeId).ToString()); s.Driver.FindElement(By.Id("AddApiKey")).Click(); s.Driver.FindElement(By.Id("Generate")).Click(); var noPermissionsApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; - await TestApiAgainstAccessToken(noPermissionsApiKey, tester, user); + await TestApiAgainstAccessToken(noPermissionsApiKey, tester, user, string.Empty); await Assert.ThrowsAnyAsync(async () => { @@ -118,13 +115,13 @@ namespace BTCPayServer.Tests //strict //selectiveStores var authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri, - new[] {Permissions.StoreManagement, Permissions.ServerManagement}).ToString(); + new[] {Permission.CanModifyStoreSettings, Permission.CanModifyServerSettings}).ToString(); s.Driver.Navigate().GoToUrl(authUrl); s.Driver.PageSource.Contains("kukksappname"); - Assert.Equal("hidden", s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("type").ToLowerInvariant()); - Assert.Equal("true", s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("value").ToLowerInvariant()); - Assert.Equal("hidden", s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("type").ToLowerInvariant()); - Assert.Equal("true",s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("value").ToLowerInvariant()); + Assert.Equal("hidden", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("type").ToLowerInvariant()); + Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("value").ToLowerInvariant()); + Assert.Equal("hidden", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("type").ToLowerInvariant()); + Assert.Equal("true",s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("value").ToLowerInvariant()); Assert.DoesNotContain("change-store-mode", s.Driver.PageSource); s.Driver.FindElement(By.Id("consent-yes")).Click(); var url = s.Driver.Url; @@ -134,20 +131,20 @@ namespace BTCPayServer.Tests var apiKeyRepo = s.Server.PayTester.GetService(); await TestApiAgainstAccessToken(results.Single(pair => pair.Key == "key").Value, tester, user, - (await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).GetPermissions()); + (await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).Permissions); authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri, - new[] {Permissions.StoreManagement, Permissions.ServerManagement}, false, true).ToString(); + new[] {Permission.CanModifyStoreSettings, Permission.CanModifyServerSettings}, false, true).ToString(); s.Driver.Navigate().GoToUrl(authUrl); Assert.DoesNotContain("kukksappname", s.Driver.PageSource); - Assert.Equal("checkbox", s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("type").ToLowerInvariant()); - Assert.Equal("true", s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("value").ToLowerInvariant()); - Assert.Equal("checkbox", s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("type").ToLowerInvariant()); - Assert.Equal("true",s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("value").ToLowerInvariant()); + Assert.Equal("checkbox", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("type").ToLowerInvariant()); + Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("value").ToLowerInvariant()); + Assert.Equal("checkbox", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("type").ToLowerInvariant()); + Assert.Equal("true",s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("value").ToLowerInvariant()); - s.SetCheckbox(s, "ServerManagementPermission", false); + s.SetCheckbox(s, "btcpay.server.canmodifyserversettings", false); Assert.Contains("change-store-mode", s.Driver.PageSource); s.Driver.FindElement(By.Id("consent-yes")).Click(); url = s.Driver.Url; @@ -155,14 +152,15 @@ namespace BTCPayServer.Tests .Select(s1 => new KeyValuePair(s1.Split("=")[0], s1.Split("=")[1])); await TestApiAgainstAccessToken(results.Single(pair => pair.Key == "key").Value, tester, user, - (await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).GetPermissions()); + (await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).Permissions); } } async Task TestApiAgainstAccessToken(string accessToken, ServerTester tester, TestAccount testAccount, - params string[] permissions) + string permissionFormatted) { + var permissions = Permission.ToPermissions(permissionFormatted); var resultUser = await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/id", tester.PayTester.HttpClient); @@ -172,49 +170,68 @@ namespace BTCPayServer.Tests var secondUser = tester.NewAccount(); secondUser.GrantAccess(); - var selectiveStorePermissions = Permissions.ExtractStorePermissionsIds(permissions); - if (permissions.Contains(Permissions.StoreManagement) || selectiveStorePermissions.Any()) + var canModifyAllStores = Permission.Create(Permission.CanModifyStoreSettings, null); + var canModifyServer = Permission.Create(Permission.CanModifyServerSettings, null); + var unrestricted = Permission.Create(Permission.Unrestricted, null); + var selectiveStorePermissions = permissions.Where(p => p.StoreId != null && p.Policy == Permission.CanModifyStoreSettings); + if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Any()) { var resultStores = await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/stores", tester.PayTester.HttpClient); - foreach (string selectiveStorePermission in selectiveStorePermissions) + foreach (var selectiveStorePermission in selectiveStorePermissions) { Assert.True(await TestApiAgainstAccessToken(accessToken, - $"{TestApiPath}/me/stores/{selectiveStorePermission}/can-edit", + $"{TestApiPath}/me/stores/{selectiveStorePermission.StoreId}/can-edit", tester.PayTester.HttpClient)); Assert.Contains(resultStores, - data => data.Id.Equals(selectiveStorePermission, StringComparison.InvariantCultureIgnoreCase)); + data => data.Id.Equals(selectiveStorePermission.StoreId, StringComparison.InvariantCultureIgnoreCase)); } - if (permissions.Contains(Permissions.StoreManagement)) + bool shouldBeAuthorized = false; + if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Contains(Permission.Create(Permission.CanViewStoreSettings, testAccount.StoreId))) { Assert.True(await TestApiAgainstAccessToken(accessToken, - $"{TestApiPath}/me/stores/actions", + $"{TestApiPath}/me/stores/{testAccount.StoreId}/can-view", + tester.PayTester.HttpClient)); + Assert.Contains(resultStores, + data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase)); + shouldBeAuthorized = true; + } + if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Contains(Permission.Create(Permission.CanModifyStoreSettings, testAccount.StoreId))) + { + Assert.True(await TestApiAgainstAccessToken(accessToken, + $"{TestApiPath}/me/stores/{testAccount.StoreId}/can-view", tester.PayTester.HttpClient)); - Assert.True(await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit", tester.PayTester.HttpClient)); Assert.Contains(resultStores, data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase)); + shouldBeAuthorized = true; } - else + + if (!shouldBeAuthorized) { await Assert.ThrowsAnyAsync(async () => { await TestApiAgainstAccessToken(accessToken, - $"{TestApiPath}/me/stores/actions", - tester.PayTester.HttpClient); + $"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit", + tester.PayTester.HttpClient); }); + await Assert.ThrowsAnyAsync(async () => + { + await TestApiAgainstAccessToken(accessToken, + $"{TestApiPath}/me/stores/{testAccount.StoreId}/can-view", + tester.PayTester.HttpClient); + }); + Assert.DoesNotContain(resultStores, + data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase)); } - - Assert.DoesNotContain(resultStores, - data => data.Id.Equals(secondUser.StoreId, StringComparison.InvariantCultureIgnoreCase)); } - else if(!permissions.Contains(Permissions.ServerManagement)) + else if(!permissions.Contains(unrestricted)) { await Assert.ThrowsAnyAsync(async () => @@ -231,7 +248,7 @@ namespace BTCPayServer.Tests tester.PayTester.HttpClient); } - if (!permissions.Contains(Permissions.ServerManagement)) + if (!permissions.Contains(unrestricted)) { await Assert.ThrowsAnyAsync(async () => { @@ -245,7 +262,7 @@ namespace BTCPayServer.Tests tester.PayTester.HttpClient); } - if (permissions.Contains(Permissions.ServerManagement)) + if (permissions.Contains(canModifyServer)) { Assert.True(await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/is-admin", diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 4d3ebb483..c0aea9463 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -38,7 +38,7 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); user.GrantAccess(); await user.MakeAdmin(); - var client = await user.CreateClient(Permissions.ServerManagement, Permissions.StoreManagement); + var client = await user.CreateClient(Permission.CanModifyServerSettings, Permission.CanModifyStoreSettings); //Get current api key var apiKeyData = await client.GetCurrentAPIKeyInfo(); Assert.NotNull(apiKeyData); @@ -97,14 +97,14 @@ namespace BTCPayServer.Tests var adminAcc = tester.NewAccount(); adminAcc.UserId = admin.Id; adminAcc.IsAdmin = true; - var adminClient = await adminAcc.CreateClient(Permissions.ProfileManagement); + var adminClient = await adminAcc.CreateClient(Permission.CanModifyProfile); // We should be forbidden to create a new user without proper admin permissions await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" })); await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true })); // However, should be ok with the server management permissions - adminClient = await adminAcc.CreateClient(Permissions.ServerManagement); + adminClient = await adminAcc.CreateClient(Permission.CanModifyServerSettings); await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" }); // Even creating new admin should be ok await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true }); @@ -112,7 +112,7 @@ namespace BTCPayServer.Tests var user1Acc = tester.NewAccount(); user1Acc.UserId = user1.Id; user1Acc.IsAdmin = false; - var user1Client = await user1Acc.CreateClient(Permissions.ServerManagement); + var user1Client = await user1Acc.CreateClient(Permission.CanModifyServerSettings); // User1 trying to get server management would still fail to create user await AssertHttpError(403, async () => await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" })); @@ -141,9 +141,9 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); user.GrantAccess(); await user.MakeAdmin(); - var clientProfile = await user.CreateClient(Permissions.ProfileManagement); - var clientServer = await user.CreateClient(Permissions.ServerManagement); - var clientInsufficient = await user.CreateClient(Permissions.StoreManagement); + var clientProfile = await user.CreateClient(Permission.CanModifyProfile); + var clientServer = await user.CreateClient(Permission.CanModifyServerSettings, Permission.CanViewProfile); + var clientInsufficient = await user.CreateClient(Permission.CanModifyStoreSettings); var apiKeyProfileUserData = await clientProfile.GetCurrentUser(); @@ -153,6 +153,7 @@ namespace BTCPayServer.Tests await Assert.ThrowsAsync(async () => await clientInsufficient.GetCurrentUser()); await clientServer.GetCurrentUser(); + await clientProfile.GetCurrentUser(); await Assert.ThrowsAsync(async () => await clientInsufficient.CreateUser(new CreateApplicationUserRequest() { diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index 5a9425e2c..efade2146 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -32,9 +32,9 @@ namespace BTCPayServer.Tests public IWebDriver Driver { get; set; } public ServerTester Server { get; set; } - public static SeleniumTester Create([CallerMemberNameAttribute] string scope = null) + public static SeleniumTester Create([CallerMemberNameAttribute] string scope = null, bool newDb = false) { - var server = ServerTester.Create(scope); + var server = ServerTester.Create(scope, newDb); return new SeleniumTester() { Server = server @@ -259,7 +259,7 @@ namespace BTCPayServer.Tests public void SetCheckbox(SeleniumTester s, string inputName, bool value) { - SetCheckbox(s.Driver.FindElement(By.Name(inputName)), value); + SetCheckbox(s.Driver.FindElement(By.Id(inputName)), value); } public void ScrollToElement(IWebElement element) diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index 44769d816..9ecfe14e4 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -38,11 +38,14 @@ namespace BTCPayServer.Tests GrantAccessAsync().GetAwaiter().GetResult(); } - public async Task MakeAdmin() + public async Task MakeAdmin(bool isAdmin = true) { var userManager = parent.PayTester.GetService>(); var u = await userManager.FindByIdAsync(UserId); - await userManager.AddToRoleAsync(u, Roles.ServerAdmin); + if (isAdmin) + await userManager.AddToRoleAsync(u, Roles.ServerAdmin); + else + await userManager.RemoveFromRoleAsync(u, Roles.ServerAdmin); IsAdmin = true; } diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 8c71cd8a8..94013f5d0 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -62,6 +62,7 @@ using BTCPayServer.U2F.Models; using BTCPayServer.Security.Bitpay; using MemoryCache = Microsoft.Extensions.Caching.Memory.MemoryCache; using Newtonsoft.Json.Schema; +using BTCPayServer.Client; namespace BTCPayServer.Tests { @@ -3000,6 +3001,22 @@ noninventoryitem: await new ApplicationDbContext(builder.Options).Database.MigrateAsync(); } + [Fact(Timeout = TestTimeout)] + [Trait("Fast", "Fast")] + public void CanUsePermission() + { + Assert.True(Permission.Create(Permission.CanModifyServerSettings).Contains(Permission.Create(Permission.CanModifyServerSettings))); + Assert.True(Permission.Create(Permission.CanModifyProfile).Contains(Permission.Create(Permission.CanViewProfile))); + Assert.True(Permission.Create(Permission.CanModifyStoreSettings).Contains(Permission.Create(Permission.CanViewStoreSettings))); + Assert.False(Permission.Create(Permission.CanViewStoreSettings).Contains(Permission.Create(Permission.CanModifyStoreSettings))); + Assert.False(Permission.Create(Permission.CanModifyServerSettings).Contains(Permission.Create(Permission.CanModifyStoreSettings))); + Assert.True(Permission.Create(Permission.Unrestricted).Contains(Permission.Create(Permission.CanModifyStoreSettings))); + Assert.True(Permission.Create(Permission.Unrestricted).Contains(Permission.Create(Permission.CanModifyStoreSettings, "abc"))); + + Assert.True(Permission.Create(Permission.CanViewStoreSettings).Contains(Permission.Create(Permission.CanViewStoreSettings, "abcd"))); + Assert.False(Permission.Create(Permission.CanModifyStoreSettings, "abcd").Contains(Permission.Create(Permission.CanModifyStoreSettings))); + } + [Fact(Timeout = TestTimeout)] [Trait("Fast", "Fast")] public void CheckRatesProvider() diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index db100f3f6..e39898639 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -223,5 +223,5 @@ <_ContentIncludedByDefault Remove="Views\Authorization\Authorize.cshtml" /> - + diff --git a/BTCPayServer/Controllers/InvoiceController.API.cs b/BTCPayServer/Controllers/InvoiceController.API.cs index 0d2f6e6e9..e95a11c17 100644 --- a/BTCPayServer/Controllers/InvoiceController.API.cs +++ b/BTCPayServer/Controllers/InvoiceController.API.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Client; using BTCPayServer.Filters; using BTCPayServer.Models; using BTCPayServer.Security; @@ -12,7 +13,7 @@ using Microsoft.AspNetCore.Mvc; namespace BTCPayServer.Controllers { [BitpayAPIConstraint] - [Authorize(Policies.CanCreateInvoice.Key, AuthenticationSchemes = AuthenticationSchemes.Bitpay)] + [Authorize(Permission.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Bitpay)] public class InvoiceControllerAPI : Controller { private InvoiceController _InvoiceController; diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 01aef66f1..9eca43d5a 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -6,6 +6,7 @@ using System.Net.Mime; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Events; using BTCPayServer.Filters; @@ -510,7 +511,7 @@ namespace BTCPayServer.Controllers [HttpPost] [Route("invoices/create")] - [Authorize(Policy = Policies.CanCreateInvoice.Key, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Permission.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [BitpayAPIConstraint(false)] public async Task CreateInvoice(CreateInvoiceModel model, CancellationToken cancellationToken) { diff --git a/BTCPayServer/Controllers/ManageController.APIKeys.cs b/BTCPayServer/Controllers/ManageController.APIKeys.cs index fa94b190d..65c9c92bb 100644 --- a/BTCPayServer/Controllers/ManageController.APIKeys.cs +++ b/BTCPayServer/Controllers/ManageController.APIKeys.cs @@ -25,11 +25,11 @@ namespace BTCPayServer.Controllers { ApiKeyDatas = await _apiKeyRepository.GetKeys(new APIKeyRepository.APIKeyQuery() { - UserId = new[] {_userManager.GetUserId(User)} + UserId = new[] { _userManager.GetUserId(User) } }) }); } - + [HttpGet("api-keys/{id}/delete")] public async Task RemoveAPIKey(string id) { @@ -96,22 +96,13 @@ namespace BTCPayServer.Controllers permissions ??= Array.Empty(); - var vm = await SetViewModelValues(new AuthorizeApiKeysViewModel() + var vm = await SetViewModelValues(new AuthorizeApiKeysViewModel(Permission.ToPermissions(permissions)) { Label = applicationName, - ServerManagementPermission = permissions.Contains(Permissions.ServerManagement), - StoreManagementPermission = permissions.Contains(Permissions.StoreManagement), - PermissionsFormatted = permissions, - PermissionValues = permissions.Where(s => - !s.Contains(Permissions.StoreManagement, StringComparison.InvariantCultureIgnoreCase) && - s != Permissions.ServerManagement) - .Select(s => new AddApiKeyViewModel.PermissionValueItem() {Permission = s, Value = true}).ToList(), ApplicationName = applicationName, SelectiveStores = selectiveStores, Strict = strict, }); - - vm.ServerManagementPermission = vm.ServerManagementPermission && vm.IsServerAdmin; return View(vm); } @@ -126,22 +117,20 @@ namespace BTCPayServer.Controllers return ar; } - - if (viewModel.PermissionsFormatted.Contains(Permissions.ServerManagement)) + if (viewModel.Strict) { - if (!viewModel.IsServerAdmin && viewModel.ServerManagementPermission) + for (int i = 0; i < viewModel.PermissionValues.Count; i++) { - viewModel.ServerManagementPermission = false; - } - - if (!viewModel.ServerManagementPermission && viewModel.Strict) - { - ModelState.AddModelError(nameof(viewModel.ServerManagementPermission), - "This permission is required for this application."); + if (viewModel.PermissionValues[i].Forbidden) + { + ModelState.AddModelError($"{viewModel.PermissionValues}[{i}].Value", + $"The permission '{viewModel.PermissionValues[i].Title}' is required for this application."); + } } } - if (viewModel.PermissionsFormatted.Contains(Permissions.StoreManagement)) + var permissions = Permission.ToPermissions(viewModel.Permissions).ToHashSet(); + if (permissions.Contains(Permission.Create(Permission.CanModifyStoreSettings))) { if (!viewModel.SelectiveStores && viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific) @@ -151,10 +140,10 @@ namespace BTCPayServer.Controllers "This application does not allow selective store permissions."); } - if (!viewModel.StoreManagementPermission && !viewModel.SpecificStores.Any() && viewModel.Strict) + if (!viewModel.StoreManagementPermission.Value && !viewModel.SpecificStores.Any() && viewModel.Strict) { ModelState.AddModelError(nameof(viewModel.StoreManagementPermission), - "This permission is required for this application."); + $"This permission '{viewModel.StoreManagementPermission.Title}' is required for this application."); } } @@ -174,8 +163,9 @@ namespace BTCPayServer.Controllers Severity = StatusMessageModel.StatusSeverity.Success, Html = $"API key generated! {key.Id}" }); - return RedirectToAction("APIKeys", new { key = key.Id}); - default: return View(viewModel); + return RedirectToAction("APIKeys", new { key = key.Id }); + default: + return View(viewModel); } } @@ -225,15 +215,15 @@ namespace BTCPayServer.Controllers return View(viewModel); case string x when x.StartsWith("remove-store", StringComparison.InvariantCultureIgnoreCase): - { - ModelState.Clear(); - var index = int.Parse( - viewModel.Command.Substring( - viewModel.Command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1), - CultureInfo.InvariantCulture); - viewModel.SpecificStores.RemoveAt(index); - return View(viewModel); - } + { + ModelState.Clear(); + var index = int.Parse( + viewModel.Command.Substring( + viewModel.Command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1), + CultureInfo.InvariantCulture); + viewModel.SpecificStores.RemoveAt(index); + return View(viewModel); + } } return null; @@ -248,53 +238,104 @@ namespace BTCPayServer.Controllers UserId = _userManager.GetUserId(User), Label = viewModel.Label }; - key.SetPermissions(GetPermissionsFromViewModel(viewModel)); + key.Permissions = string.Join(";", GetPermissionsFromViewModel(viewModel).Select(p => p.ToString()).Distinct().ToArray()); await _apiKeyRepository.CreateKey(key); return key; } - private IEnumerable GetPermissionsFromViewModel(AddApiKeyViewModel viewModel) + private IEnumerable GetPermissionsFromViewModel(AddApiKeyViewModel viewModel) { - var permissions = viewModel.PermissionValues.Where(tuple => tuple.Value).Select(tuple => tuple.Permission).ToList(); - - if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific) + List permissions = new List(); + foreach (var p in viewModel.PermissionValues.Where(tuple => tuple.Value && !tuple.Forbidden)) { - permissions.AddRange(viewModel.SpecificStores.Select(Permissions.GetStorePermission)); + if (Permission.TryCreatePermission(p.Permission, null, out var pp)) + permissions.Add(pp); } - else if (viewModel.StoreManagementPermission) + if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.AllStores && viewModel.StoreManagementPermission.Value) { - permissions.Add(Permissions.StoreManagement); + permissions.Add(Permission.Create(Permission.CanModifyStoreSettings)); } - - if (viewModel.IsServerAdmin && viewModel.ServerManagementPermission) + else if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific) { - permissions.Add(Permissions.ServerManagement); + permissions.AddRange(viewModel.SpecificStores.Select(s => Permission.Create(Permission.CanModifyStoreSettings, s))); } - return permissions.Distinct(); } private async Task SetViewModelValues(T viewModel) where T : AddApiKeyViewModel { viewModel.Stores = await _StoreRepository.GetStoresByUserId(_userManager.GetUserId(User)); - viewModel.IsServerAdmin = - (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings.Key)).Succeeded; - viewModel.PermissionValues ??= Permissions.GetAllPermissionKeys().Where(s => - !s.Contains(Permissions.StoreManagement, StringComparison.InvariantCultureIgnoreCase) && - s != Permissions.ServerManagement) - .Select(s => new AddApiKeyViewModel.PermissionValueItem() {Permission = s, Value = true}).ToList(); + var isAdmin = (await _authorizationService.AuthorizeAsync(User, Permission.CanModifyServerSettings)).Succeeded; + viewModel.PermissionValues ??= Permission.AllPolicies.Where(p => p != Permission.CanModifyStoreSettings) + .Select(s => new AddApiKeyViewModel.PermissionValueItem() { Permission = s, Value = false }).ToList(); + if (!isAdmin) + { + foreach (var p in viewModel.PermissionValues) + { + if (p.Permission == Permission.CanCreateUser || + p.Permission == Permission.CanModifyServerSettings) + { + p.Forbidden = true; + } + } + } return viewModel; } public class AddApiKeyViewModel { + public AddApiKeyViewModel() + { + StoreManagementPermission = new PermissionValueItem() + { + Permission = Permission.CanModifyStoreSettings, + Value = false + }; + StoreManagementSelectivePermission = new PermissionValueItem() + { + Permission = $"{Permission.CanModifyStoreSettings}:", + Value = true + }; + } + public AddApiKeyViewModel(IEnumerable permissions):this() + { + StoreManagementPermission.Value = permissions.Any(p => p.Policy == Permission.CanModifyStoreSettings && p.StoreId == null); + PermissionValues = permissions.Where(p => p.Policy != Permission.CanModifyStoreSettings) + .Select(p => new PermissionValueItem() { Permission = p.ToString(), Value = true }) + .ToList(); + } + + public IEnumerable GetPermissions() + { + if (!(PermissionValues is null)) + { + foreach (var p in PermissionValues.Where(o => o.Value)) + { + if (Permission.TryCreatePermission(p.Permission, null, out var pp)) + yield return pp; + } + } + if (this.StoreMode == ApiKeyStoreMode.AllStores) + { + if (StoreManagementPermission.Value) + yield return Permission.Create(Permission.CanModifyStoreSettings); + } + else if (this.StoreMode == ApiKeyStoreMode.Specific && SpecificStores is List) + { + foreach (var p in SpecificStores) + { + if (Permission.TryCreatePermission(Permission.CanModifyStoreSettings, p, out var pp)) + yield return pp; + } + } + } public string Label { get; set; } public StoreData[] Stores { get; set; } + ApiKeyStoreMode _StoreMode; public ApiKeyStoreMode StoreMode { get; set; } public List SpecificStores { get; set; } = new List(); - public bool IsServerAdmin { get; set; } - public bool ServerManagementPermission { get; set; } - public bool StoreManagementPermission { get; set; } + public PermissionValueItem StoreManagementPermission { get; set; } + public PermissionValueItem StoreManagementSelectivePermission { get; set; } public string Command { get; set; } public List PermissionValues { get; set; } @@ -306,29 +347,52 @@ namespace BTCPayServer.Controllers public class PermissionValueItem { + public static readonly Dictionary PermissionDescriptions = new Dictionary() + { + {BTCPayServer.Client.Permission.Unrestricted, ("Unrestricted access", "The app will have unrestricted access to your account.")}, + {BTCPayServer.Client.Permission.CanCreateUser, ("Create new users", "The app will be able to create new users on this server.")}, + {BTCPayServer.Client.Permission.CanModifyStoreSettings, ("Modify your stores", "The app will be able to create, view and modify, delete and create new invoices on the all your stores.")}, + {BTCPayServer.Client.Permission.CanViewStoreSettings, ("View your stores", "The app will be able to create, view all your stores.")}, + {$"{BTCPayServer.Client.Permission.CanModifyStoreSettings}:", ("Manage selected stores", "The app will be able to view, modify, delete and create new invoices on the selected stores.")}, + {BTCPayServer.Client.Permission.CanModifyServerSettings, ("Manage your server", "The app will have total control on your server")}, + {BTCPayServer.Client.Permission.CanViewProfile, ("View your profile", "The app will be able to view your user profile.")}, + {BTCPayServer.Client.Permission.CanModifyProfile, ("Manage your profile", "The app will be able to view and modify your user profile.")}, + {BTCPayServer.Client.Permission.CanCreateInvoice, ("Create an invoice", "The app will be able to create new invoice.")}, + }; + public string Title + { + get + { + return PermissionDescriptions[Permission].Title; + } + } + public string Description + { + get + { + return PermissionDescriptions[Permission].Description; + } + } public string Permission { get; set; } public bool Value { get; set; } + public bool Forbidden { get; set; } } } public class AuthorizeApiKeysViewModel : AddApiKeyViewModel { + public AuthorizeApiKeysViewModel() + { + + } + public AuthorizeApiKeysViewModel(IEnumerable permissions) : base(permissions) + { + Permissions = string.Join(';', permissions.Select(p => p.ToString()).ToArray()); + } public string ApplicationName { get; set; } public bool Strict { get; set; } public bool SelectiveStores { get; set; } public string Permissions { get; set; } - - public string[] PermissionsFormatted - { - get - { - return Permissions?.Split(";", StringSplitOptions.RemoveEmptyEntries)?? Array.Empty(); - } - set - { - Permissions = string.Join(';', value ?? Array.Empty()); - } - } } diff --git a/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs b/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs index ab229a6f7..cc7b7e7de 100644 --- a/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs +++ b/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs @@ -1,4 +1,6 @@ using System.Threading.Tasks; +using System.Linq; +using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Data; using BTCPayServer.Security; @@ -43,7 +45,7 @@ namespace BTCPayServer.Controllers.RestApi.ApiKeys { return new ApiKeyData() { - Permissions = data.GetPermissions(), + Permissions = Permission.ToPermissions(data.Permissions).Select(c => c.ToString()).ToArray(), ApiKey = data.Id, UserId = data.UserId, Label = data.Label diff --git a/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs b/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs index d47a732dd..ac81ef575 100644 --- a/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs +++ b/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Security; using BTCPayServer.Security.APIKeys; @@ -27,45 +28,45 @@ namespace BTCPayServer.Controllers.RestApi } [HttpGet("me/id")] + [Authorize(Policy = Permission.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] public string GetCurrentUserId() { return _userManager.GetUserId(User); } [HttpGet("me")] + [Authorize(Policy = Permission.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] public async Task GetCurrentUser() { return await _userManager.GetUserAsync(User); } [HttpGet("me/is-admin")] - [Authorize(Policy = Policies.CanModifyServerSettings.Key, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(Policy = Permission.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] public bool AmIAnAdmin() { return true; } [HttpGet("me/stores")] - [Authorize(Policy = Policies.CanListStoreSettings.Key, - AuthenticationSchemes = AuthenticationSchemes.ApiKey)] - public async Task GetCurrentUserStores() + [Authorize(Policy = Permission.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + public StoreData[] GetCurrentUserStores() { - return await User.GetStores(_userManager, _storeRepository); + return this.HttpContext.GetStoresData(); } - - [HttpGet("me/stores/actions")] - [Authorize(Policy = Policies.CanModifyStoreSettings.Key, + + [HttpGet("me/stores/{storeId}/can-view")] + [Authorize(Policy = Permission.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] - public bool CanDoNonImplicitStoreActions() + public bool CanViewStore(string storeId) { return true; } - [HttpGet("me/stores/{storeId}/can-edit")] - [Authorize(Policy = Policies.CanModifyStoreSettings.Key, + [Authorize(Policy = Permission.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] - public bool CanEdit(string storeId) + public bool CanEditStore(string storeId) { return true; } diff --git a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs index 0564075d9..7c2763d32 100644 --- a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs +++ b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs @@ -18,6 +18,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using NicolasDorier.RateLimits; +using BTCPayServer.Client; namespace BTCPayServer.Controllers.RestApi.Users { @@ -54,7 +55,7 @@ namespace BTCPayServer.Controllers.RestApi.Users _authorizationService = authorizationService; } - [Authorize(Policy = Policies.CanModifyProfile.Key, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(Policy = Permission.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] [HttpGet("~/api/v1/users/me")] public async Task> GetCurrentUser() { @@ -86,26 +87,21 @@ namespace BTCPayServer.Controllers.RestApi.Users if (anyAdmin && request.IsAdministrator is true && !isAuth) return Forbid(AuthenticationSchemes.ApiKey); // You are de-facto admin if there is no other admin, else you need to be auth and pass policy requirements - bool isAdmin = anyAdmin ? (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanModifyServerSettings.Key))).Succeeded + bool isAdmin = anyAdmin ? (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Permission.CanModifyServerSettings))).Succeeded && isAuth : true; // You need to be admin to create an admin if (request.IsAdministrator is true && !isAdmin) return Forbid(AuthenticationSchemes.ApiKey); - var canCreateUser = (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanCreateUser.Key))).Succeeded; if (!isAdmin && policies.LockSubscription) { // If we are not admin and subscriptions are locked, we need to check the Policies.CanCreateUser.Key permission + var canCreateUser = (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Permission.CanCreateUser))).Succeeded; if (!isAuth || !canCreateUser) return Forbid(AuthenticationSchemes.ApiKey); } - // TODO: Check if needed to reenable - // Forbid non-admin users without CanCreateUser permission to create accounts - //if (isAuth && !isAdmin && !canCreateUser) - // return Forbid(AuthenticationSchemes.ApiKey); - var user = new ApplicationUser { UserName = request.Email, diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index b055ce0c1..28b7df6cb 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -35,10 +35,11 @@ using BTCPayServer.Services.Apps; using Microsoft.AspNetCore.Mvc.Rendering; using BTCPayServer.Data; using Microsoft.EntityFrameworkCore; +using BTCPayServer.Client; namespace BTCPayServer.Controllers { - [Authorize(Policy = BTCPayServer.Security.Policies.CanModifyServerSettings.Key, + [Authorize(Policy = Permission.CanModifyServerSettings, AuthenticationSchemes = BTCPayServer.Security.AuthenticationSchemes.Cookie)] public partial class ServerController : Controller { diff --git a/BTCPayServer/Controllers/StoresController.BTCLike.cs b/BTCPayServer/Controllers/StoresController.BTCLike.cs index 8b33bd5bf..eebebf054 100644 --- a/BTCPayServer/Controllers/StoresController.BTCLike.cs +++ b/BTCPayServer/Controllers/StoresController.BTCLike.cs @@ -24,6 +24,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using BTCPayServer.Logging; using Microsoft.Extensions.Logging; +using BTCPayServer.Client; namespace BTCPayServer.Controllers { @@ -386,7 +387,7 @@ namespace BTCPayServer.Controllers private async Task CanUseHotWallet() { - var isAdmin = (await _authorizationService.AuthorizeAsync(User, BTCPayServer.Security.Policies.CanModifyServerSettings.Key)).Succeeded; + var isAdmin = (await _authorizationService.AuthorizeAsync(User, Permission.CanModifyServerSettings)).Succeeded; if (isAdmin) return true; var policies = await _settingsRepository.GetSettingAsync(); diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index dda5a65ee..fcce5c962 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Client; using BTCPayServer.Configuration; using BTCPayServer.Data; using BTCPayServer.HostedServices; @@ -33,7 +34,7 @@ namespace BTCPayServer.Controllers { [Route("stores")] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] - [Authorize(Policy = Policies.CanModifyStoreSettings.Key, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Permission.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [AutoValidateAntiforgeryToken] public partial class StoresController : Controller { diff --git a/BTCPayServer/Controllers/VaultController.cs b/BTCPayServer/Controllers/VaultController.cs index 1c29d9105..edb4211a3 100644 --- a/BTCPayServer/Controllers/VaultController.cs +++ b/BTCPayServer/Controllers/VaultController.cs @@ -7,6 +7,7 @@ using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Hwi; using BTCPayServer.ModelBinders; @@ -127,7 +128,7 @@ namespace BTCPayServer.Controllers } await websocketHelper.Send("{ \"info\": \"ready\"}", cancellationToken); o = JObject.Parse(await websocketHelper.NextMessageAsync(cancellationToken)); - var authorization = await _authorizationService.AuthorizeAsync(User, Policies.CanModifyStoreSettings.Key); + var authorization = await _authorizationService.AuthorizeAsync(User, Permission.CanModifyStoreSettings); if (!authorization.Succeeded) { await websocketHelper.Send("{ \"error\": \"not-authorized\"}", cancellationToken); diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index e4dc74e6d..bce2f827f 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -6,6 +6,7 @@ using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.HostedServices; using BTCPayServer.ModelBinders; @@ -30,7 +31,7 @@ using Newtonsoft.Json; namespace BTCPayServer.Controllers { [Route("wallets")] - [Authorize(Policy = Policies.CanModifyStoreSettings.Key, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Permission.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [AutoValidateAntiforgeryToken] public partial class WalletsController : Controller { @@ -366,7 +367,7 @@ namespace BTCPayServer.Controllers private async Task CanUseHotWallet() { - var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings.Key)).Succeeded; + var isAdmin = (await _authorizationService.AuthorizeAsync(User, Permission.CanModifyServerSettings)).Succeeded; if (isAdmin) return true; var policies = await _settingsRepository.GetSettingAsync(); @@ -839,7 +840,7 @@ namespace BTCPayServer.Controllers var vm = new RescanWalletModel(); vm.IsFullySync = _dashboard.IsFullySynched(walletId.CryptoCode, out var unused); - vm.IsServerAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings.Key)).Succeeded; + vm.IsServerAdmin = (await _authorizationService.AuthorizeAsync(User, Permission.CanModifyServerSettings)).Succeeded; vm.IsSupportedByCurrency = _dashboard.Get(walletId.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanScanTxoutSet == true; var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode); var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.AccountDerivation); @@ -869,7 +870,7 @@ namespace BTCPayServer.Controllers [HttpPost] [Route("{walletId}/rescan")] - [Authorize(Policy = Policies.CanModifyServerSettings.Key, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Permission.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] public async Task WalletRescan( [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, RescanWalletModel vm) diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 1d1736423..90e4305f1 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -425,6 +425,15 @@ namespace BTCPayServer ctx.Items["BTCPAY.STOREDATA"] = storeData; } + public static StoreData[] GetStoresData(this HttpContext ctx) + { + return ctx.Items.TryGet("BTCPAY.STORESDATA") as StoreData[]; + } + public static void SetStoresData(this HttpContext ctx, StoreData[] storeData) + { + ctx.Items["BTCPAY.STORESDATA"] = storeData; + } + private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }; public static string ToJson(this object o) { diff --git a/BTCPayServer/Security/APIKeys/APIKeyAuthenticationHandler.cs b/BTCPayServer/Security/APIKeys/APIKeyAuthenticationHandler.cs index 056b48180..07eff055d 100644 --- a/BTCPayServer/Security/APIKeys/APIKeyAuthenticationHandler.cs +++ b/BTCPayServer/Security/APIKeys/APIKeyAuthenticationHandler.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; +using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Security.Bitpay; using BTCPayServer.Services.Stores; @@ -44,11 +45,8 @@ namespace BTCPayServer.Security.APIKeys } List claims = new List(); - claims.Add(new Claim(_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, key.UserId)); - claims.AddRange(key.GetPermissions() - .Select(permission => new Claim(APIKeyConstants.ClaimTypes.Permissions, permission))); - + claims.AddRange(Permission.ToPermissions(key.Permissions).Select(permission => new Claim(APIKeyConstants.ClaimTypes.Permission, permission.ToString()))); return AuthenticateResult.Success(new AuthenticationTicket( new ClaimsPrincipal(new ClaimsIdentity(claims, APIKeyConstants.AuthenticationType)), APIKeyConstants.AuthenticationType)); } diff --git a/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs b/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs index c693c2be1..0c06c5760 100644 --- a/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs +++ b/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; @@ -35,52 +36,54 @@ namespace BTCPayServer.Security.APIKeys bool success = false; switch (requirement.Policy) { - case Policies.CanModifyProfile.Key: - success = context.HasPermissions(Permissions.ProfileManagement); + case Permission.CanModifyProfile: + case Permission.CanViewProfile: + success = context.HasPermission(Permission.Create(requirement.Policy)); break; - case Policies.CanListStoreSettings.Key: - var selectiveStorePermissions = - Permissions.ExtractStorePermissionsIds(context.GetPermissions()); - success = context.HasPermissions(Permissions.StoreManagement) || - selectiveStorePermissions.Any(); - break; - case Policies.CanModifyStoreSettings.Key: - string storeId = _HttpContext.GetImplicitStoreId(); - if (!context.HasPermissions(Permissions.StoreManagement) && - !context.HasPermissions(Permissions.GetStorePermission(storeId))) - break; - if (storeId == null) + case Permission.CanViewStoreSettings: + case Permission.CanModifyStoreSettings: + var storeId = _HttpContext.GetImplicitStoreId(); + var userid = _userManager.GetUserId(context.User); + // Specific store action + if (storeId != null) { - success = true; + if (context.HasPermission(Permission.Create(requirement.Policy, storeId))) + { + if (string.IsNullOrEmpty(userid)) + break; + var store = await _storeRepository.FindStore((string)storeId, userid); + if (store == null) + break; + success = true; + _HttpContext.SetStoreData(store); + } } else { - var userid = _userManager.GetUserId(context.User); - if (string.IsNullOrEmpty(userid)) + var stores = await _storeRepository.GetStoresByUserId(userid); + List permissionedStores = new List(); + foreach (var store in stores) + { + if (context.HasPermission(Permission.Create(requirement.Policy, store.Id))) + permissionedStores.Add(store); + } + _HttpContext.SetStoresData(stores.ToArray()); + success = true; + } + break; + case Permission.CanCreateUser: + case Permission.CanModifyServerSettings: + if (context.HasPermission(Permission.Create(requirement.Policy))) + { + var user = await _userManager.GetUserAsync(context.User); + if (user == null) break; - var store = await _storeRepository.FindStore((string)storeId, userid); - if (store == null) + if (!await _userManager.IsInRoleAsync(user, Roles.ServerAdmin)) break; success = true; - _HttpContext.SetStoreData(store); } - break; - case Policies.CanCreateUser.Key: - case Policies.CanModifyServerSettings.Key: - if (!context.HasPermissions(Permissions.ServerManagement)) - break; - // For this authorization, we still check in database because it is super sensitive. - success = await IsUserAdmin(context.User); - break; - } - - //if you do not have the specific permissions, BUT you have server management, we enable god mode - if (!success && context.HasPermissions(Permissions.ServerManagement) && - requirement.Policy != Policies.CanModifyServerSettings.Key) - { - success = await IsUserAdmin(context.User); } if (success) @@ -88,15 +91,5 @@ namespace BTCPayServer.Security.APIKeys context.Succeed(requirement); } } - - private async Task IsUserAdmin(ClaimsPrincipal contextUser) - { - var user = await _userManager.GetUserAsync(contextUser); - if (user == null) - return false; - if (!await _userManager.IsInRoleAsync(user, Roles.ServerAdmin)) - return false; - return true; - } } } diff --git a/BTCPayServer/Security/APIKeys/APIKeyConstants.cs b/BTCPayServer/Security/APIKeys/APIKeyConstants.cs index f56598c9c..3ec317a53 100644 --- a/BTCPayServer/Security/APIKeys/APIKeyConstants.cs +++ b/BTCPayServer/Security/APIKeys/APIKeyConstants.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using BTCPayServer.Client; namespace BTCPayServer.Security.APIKeys { @@ -8,19 +9,7 @@ namespace BTCPayServer.Security.APIKeys public static class ClaimTypes { - public const string Permissions = nameof(APIKeys) + "." + nameof(Permissions); - } - - public static class Permissions - { - public static readonly Dictionary PermissionDescriptions = new Dictionary() - { - {Client.Permissions.StoreManagement, ("Manage your stores", "The app will be able to create, modify and delete all your stores.")}, - {$"{nameof(Client.Permissions.StoreManagement)}:", ("Manage selected stores", "The app will be able to modify and delete selected stores.")}, - {Client.Permissions.ServerManagement, ("Manage your server", "The app will have total control on your server")}, - {Client.Permissions.ProfileManagement, ("Manage your profile", "The app will be able to view and modify your user profile.")}, - }; - + public const string Permission = "APIKey.Permission"; } } } diff --git a/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs b/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs index 6f5429011..5cf617329 100644 --- a/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs +++ b/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs @@ -29,22 +29,6 @@ namespace BTCPayServer.Security.APIKeys return false; } - public static Task GetStores(this ClaimsPrincipal claimsPrincipal, - UserManager userManager, StoreRepository storeRepository) - { - var permissions = - claimsPrincipal.Claims.Where(claim => claim.Type == APIKeyConstants.ClaimTypes.Permissions) - .Select(claim => claim.Value).ToList(); - - if (permissions.Contains(Permissions.StoreManagement)) - { - return storeRepository.GetStoresByUserId(userManager.GetUserId(claimsPrincipal)); - } - - var storeIds = Permissions.ExtractStorePermissionsIds(permissions); - return storeRepository.GetStoresByUserId(userManager.GetUserId(claimsPrincipal), storeIds); - } - public static AuthenticationBuilder AddAPIKeyAuthentication(this AuthenticationBuilder builder) { builder.AddScheme(AuthenticationSchemes.ApiKey, @@ -62,15 +46,24 @@ namespace BTCPayServer.Security.APIKeys public static string[] GetPermissions(this AuthorizationHandlerContext context) { return context.User.Claims.Where(c => - c.Type.Equals(APIKeyConstants.ClaimTypes.Permissions, StringComparison.InvariantCultureIgnoreCase)) + c.Type.Equals(APIKeyConstants.ClaimTypes.Permission, StringComparison.InvariantCultureIgnoreCase)) .Select(claim => claim.Value).ToArray(); } - public static bool HasPermissions(this AuthorizationHandlerContext context, params string[] scopes) + public static bool HasPermission(this AuthorizationHandlerContext context, Permission permission) { - return scopes.All(s => context.User.HasClaim(c => - c.Type.Equals(APIKeyConstants.ClaimTypes.Permissions, StringComparison.InvariantCultureIgnoreCase) && - c.Value.Split(' ').Contains(s))); + foreach (var claim in context.User.Claims.Where(c => + c.Type.Equals(APIKeyConstants.ClaimTypes.Permission, StringComparison.InvariantCultureIgnoreCase))) + { + if (Permission.TryParse(claim.Value, out var claimPermission)) + { + if (claimPermission.Contains(permission)) + { + return true; + } + } + } + return false; } } } diff --git a/BTCPayServer/Security/Bitpay/BitpayAuthorizationHandler.cs b/BTCPayServer/Security/Bitpay/BitpayAuthorizationHandler.cs index 42bfefa3d..30b100e14 100644 --- a/BTCPayServer/Security/Bitpay/BitpayAuthorizationHandler.cs +++ b/BTCPayServer/Security/Bitpay/BitpayAuthorizationHandler.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Authentication; using BTCPayServer.Services; using BTCPayServer.Security.Bitpay; +using BTCPayServer.Client; namespace BTCPayServer.Security.Bitpay { @@ -54,7 +55,7 @@ namespace BTCPayServer.Security.Bitpay var anyoneCanInvoice = store.GetStoreBlob().AnyoneCanInvoice; switch (requirement.Policy) { - case Policies.CanCreateInvoice.Key: + case Permission.CanCreateInvoice: if (!isAnonymous || (isAnonymous && anyoneCanInvoice)) { context.Succeed(requirement); diff --git a/BTCPayServer/Security/CookieAuthorizationHandler.cs b/BTCPayServer/Security/CookieAuthorizationHandler.cs index c844b2c46..9f5c5e603 100644 --- a/BTCPayServer/Security/CookieAuthorizationHandler.cs +++ b/BTCPayServer/Security/CookieAuthorizationHandler.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Primitives; +using BTCPayServer.Client; namespace BTCPayServer.Security { @@ -35,7 +36,7 @@ namespace BTCPayServer.Security var isAdmin = context.User.IsInRole(Roles.ServerAdmin); switch (requirement.Policy) { - case Policies.CanModifyServerSettings.Key: + case Permission.CanModifyServerSettings: if (isAdmin) context.Succeed(requirement); return; @@ -56,11 +57,11 @@ namespace BTCPayServer.Security bool success = false; switch (requirement.Policy) { - case Policies.CanModifyStoreSettings.Key: + case Permission.CanModifyStoreSettings: if (store.Role == StoreRoles.Owner || isAdmin) success = true; break; - case Policies.CanCreateInvoice.Key: + case Permission.CanCreateInvoice: if (store.Role == StoreRoles.Owner || store.Role == StoreRoles.Guest || isAdmin || diff --git a/BTCPayServer/Security/Policies.cs b/BTCPayServer/Security/Policies.cs index 5e48517c1..b4cfccc5a 100644 --- a/BTCPayServer/Security/Policies.cs +++ b/BTCPayServer/Security/Policies.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Authorization; +using BTCPayServer.Client; +using Microsoft.AspNetCore.Authorization; namespace BTCPayServer.Security { @@ -6,14 +7,11 @@ namespace BTCPayServer.Security { public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options) { - options.AddPolicy(CanModifyStoreSettings.Key); - options.AddPolicy(CanListStoreSettings.Key); - options.AddPolicy(CanCreateInvoice.Key); + foreach (var p in Permission.AllPolicies) + { + options.AddPolicy(p); + } options.AddPolicy(CanGetRates.Key); - options.AddPolicy(CanModifyServerSettings.Key); - options.AddPolicy(CanModifyServerSettings.Key); - options.AddPolicy(CanModifyProfile.Key); - options.AddPolicy(CanCreateUser.Key); return options; } @@ -21,36 +19,9 @@ namespace BTCPayServer.Security { options.AddPolicy(policy, o => o.AddRequirements(new PolicyRequirement(policy))); } - - public class CanModifyServerSettings - { - public const string Key = "btcpay.store.canmodifyserversettings"; - } - public class CanModifyProfile - { - public const string Key = "btcpay.store.canmodifyprofile"; - } - public class CanModifyStoreSettings - { - public const string Key = "btcpay.store.canmodifystoresettings"; - } - public class CanListStoreSettings - { - public const string Key = "btcpay.store.canliststoresettings"; - } - public class CanCreateInvoice - { - public const string Key = "btcpay.store.cancreateinvoice"; - } - public class CanGetRates { public const string Key = "btcpay.store.cangetrates"; } - - public class CanCreateUser - { - public const string Key = "btcpay.store.cancreateuser"; - } } } diff --git a/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs b/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs index 80126cd3a..7cccf57fa 100644 --- a/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs +++ b/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs @@ -20,14 +20,15 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using BTCPayServer.Filters; +using BTCPayServer.Client; namespace BTCPayServer.Services.Altcoins.Monero.UI { [Route("stores/{storeId}/monerolike")] [OnlyIfSupportAttribute("XMR")] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] - [Authorize(Policy = Policies.CanModifyStoreSettings.Key, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - [Authorize(Policy = Policies.CanModifyServerSettings.Key, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Permission.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Permission.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] public class MoneroLikeStoreController : Controller { private readonly MoneroLikeConfiguration _MoneroLikeConfiguration; diff --git a/BTCPayServer/Views/Manage/APIKeys.cshtml b/BTCPayServer/Views/Manage/APIKeys.cshtml index ac197f44d..2ff26577e 100644 --- a/BTCPayServer/Views/Manage/APIKeys.cshtml +++ b/BTCPayServer/Views/Manage/APIKeys.cshtml @@ -1,3 +1,4 @@ +@namespace BTCPayServer.Client @model BTCPayServer.Controllers.ManageController.ApiKeysViewModel @{ ViewData.SetActivePageAndTitle(ManageNavPages.APIKeys, "Manage your API Keys"); @@ -27,7 +28,7 @@ } else { - @string.Join(", ", keyData.GetPermissions()) + @string.Join(", ", Permission.ToPermissions(keyData.Permissions).Select(c => c.ToString()).Distinct().ToArray()) } diff --git a/BTCPayServer/Views/Manage/AddApiKey.cshtml b/BTCPayServer/Views/Manage/AddApiKey.cshtml index 13791963a..5471ae6e8 100644 --- a/BTCPayServer/Views/Manage/AddApiKey.cshtml +++ b/BTCPayServer/Views/Manage/AddApiKey.cshtml @@ -5,16 +5,6 @@ @{ ViewData.SetActivePageAndTitle(ManageNavPages.APIKeys, "Add API Key"); - - string GetDescription(string permission) - { - return APIKeyConstants.Permissions.PermissionDescriptions[permission].Description; - } - - string GetTitle(string permission) - { - return APIKeyConstants.Permissions.PermissionDescriptions[permission].Description; - } }

    @ViewData["Title"]

    @@ -26,53 +16,47 @@
    - +
    - +
    - @if (Model.IsServerAdmin) - { -
    - - - -

    @GetDescription(Permissions.ServerManagement).

    -
    - } - + @for (int i = 0; i < Model.PermissionValues.Count; i++) { + @if (!Model.PermissionValues[i].Forbidden) + {
    - - + + -

    @GetDescription(Model.PermissionValues[i].Permission).

    +

    @Model.PermissionValues[i].Description

    - } + } + } @if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores) { -
    - @Html.CheckBoxFor(model => model.StoreManagementPermission, new Dictionary() {{"class", "form-check-inline"}}) +
    + - - -

    @GetDescription(Permissions.StoreManagement).

    - -
    + + +

    @Model.StoreManagementPermission.Description

    + +
    } else if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.Specific) {
  • -
    @GetTitle(Permissions.StoreManagement + ":")
    -

    @GetDescription(Permissions.StoreManagement + ":").

    +
    @Model.StoreManagementSelectivePermission.Title
    +

    @Model.StoreManagementSelectivePermission.Description

  • @if (!Model.Stores.Any()) diff --git a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml index 8ba022d87..6a431637c 100644 --- a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml +++ b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml @@ -6,24 +6,17 @@ @{ Layout = "_Layout"; ViewData["Title"] = $"Authorize {(Model.ApplicationName ?? "Application")}"; - - string GetDescription(string permission) - { - return APIKeyConstants.Permissions.PermissionDescriptions[permission].Description; - } - - string GetTitle(string permission) - { - return APIKeyConstants.Permissions.PermissionDescriptions[permission].Description; - } + var permissions = Permission.ToPermissions(Model.Permissions); + var hasStorePermission = permissions.Any(p => p.Policy == Permission.CanModifyStoreSettings); } - - - - + + + + +
    @@ -39,76 +32,60 @@
    - +
    - @if (!Model.PermissionsFormatted.Any()) + @if (!permissions.Any()) {
    -

    There are no associated permissions to the API key being requested here. The application cannot do anything with your BTCPay account other than validating your account exists.

    +

    There are no associated permissions to the API key being requested here. The application cannot do anything with your BTCPay account other than validating your account exists.

    } - @if (Model.PermissionsFormatted.Contains(Permissions.ServerManagement) && (Model.IsServerAdmin || Model.Strict)) - { -
    - @if (Model.Strict || !Model.IsServerAdmin) - { - - - } - else - { - - } - - @if (!Model.IsServerAdmin) - { - - The server management permission is being requested but your account is not an administrator - - } - - -

    @GetDescription(Permissions.ServerManagement).

    -
    - } @for (int i = 0; i < Model.PermissionValues.Count; i++) {
    - @if (Model.Strict || !Model.IsServerAdmin) + @if (Model.Strict) { - - + + } else { - + } - - -

    @GetDescription(Model.PermissionValues[i].Permission).

    + + + @if (Model.PermissionValues[i].Forbidden) + { +
    + + This permission is not available for your account. + + } +

    @Model.PermissionValues[i].Description

    } - @if (Model.PermissionsFormatted.Contains(Permissions.StoreManagement)) + @if (hasStorePermission) { @if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores) {
    @if (Model.Strict) { - - + + } else { - + } - + +
    -

    @GetDescription(Permissions.StoreManagement).

    +

    @Model.StoreManagementPermission.Description

    @if (Model.SelectiveStores) { @@ -119,8 +96,8 @@ {
  • -
    @GetTitle(Permissions.StoreManagement + ":")
    -

    @GetDescription(Permissions.StoreManagement + ":").

    +
    @Model.StoreManagementSelectivePermission.Title
    +

    @Model.StoreManagementSelectivePermission.Description

  • @if (!Model.Stores.Any()) From d72139c2c1036496ce2aa9887931642735a5f7cf Mon Sep 17 00:00:00 2001 From: Kukks Date: Thu, 19 Mar 2020 16:00:33 +0100 Subject: [PATCH 296/810] Fix liquid asset BIP21 decimal precision The way liquid assets decimal precision works is just an ui layer. Each unit of an asset is actually 1sat. Fixes https://github.com/Blockstream/green_android/issues/86 --- .../Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs b/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs index 8ab4dc883..45ceed889 100644 --- a/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs +++ b/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs @@ -25,7 +25,7 @@ namespace BTCPayServer public override string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue) { - return $"{base.GenerateBIP21(cryptoInfoAddress, cryptoInfoDue)}&assetid={AssetId}"; + return $"{base.GenerateBIP21(cryptoInfoAddress, new Money(long.Parse(cryptoInfoDue.ToString(false, true).Replace(".", ""))))}&assetid={AssetId}"; } } } From 46edc281b6ab88c79f80b0500693453e3837372c Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 20 Mar 2020 13:22:10 +0900 Subject: [PATCH 297/810] Fix tests --- BTCPayServer.Tests/ApiKeysTests.cs | 58 +++++++++++++++++++----------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/BTCPayServer.Tests/ApiKeysTests.cs b/BTCPayServer.Tests/ApiKeysTests.cs index e4917fa73..322ccd31d 100644 --- a/BTCPayServer.Tests/ApiKeysTests.cs +++ b/BTCPayServer.Tests/ApiKeysTests.cs @@ -5,6 +5,7 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using BTCPayServer.Client; +using BTCPayServer.Client.Models; using BTCPayServer.Data; using BTCPayServer.Security.APIKeys; using BTCPayServer.Tests.Logging; @@ -23,7 +24,7 @@ namespace BTCPayServer.Tests public const string TestApiPath = "api/test/apikey"; public ApiKeysTests(ITestOutputHelper helper) { - Logs.Tester = new XUnitLog(helper) {Name = "Tests"}; + Logs.Tester = new XUnitLog(helper) { Name = "Tests" }; Logs.LogProvider = new XUnitLogProvider(helper); } @@ -47,7 +48,7 @@ namespace BTCPayServer.Tests s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password); s.GoToProfile(ManageNavPages.APIKeys); s.Driver.FindElement(By.Id("AddApiKey")).Click(); - + //not an admin, so this permission should not show Assert.DoesNotContain("btcpay.server.canmodifyserversettings", s.Driver.PageSource); await user.MakeAdmin(); @@ -61,11 +62,12 @@ namespace BTCPayServer.Tests //server management should show now s.SetCheckbox(s, "btcpay.server.canmodifyserversettings", true); s.SetCheckbox(s, "btcpay.store.canmodifystoresettings", true); + s.SetCheckbox(s, "btcpay.user.canviewprofile", true); s.Driver.FindElement(By.Id("Generate")).Click(); var superApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; //this api key has access to everything - await TestApiAgainstAccessToken(superApiKey, tester, user, $"{Permission.CanModifyServerSettings};{Permission.CanModifyStoreSettings}"); + await TestApiAgainstAccessToken(superApiKey, tester, user, $"{Permission.CanModifyServerSettings};{Permission.CanModifyStoreSettings};{Permission.CanViewProfile}"); s.Driver.FindElement(By.Id("AddApiKey")).Click(); @@ -115,13 +117,13 @@ namespace BTCPayServer.Tests //strict //selectiveStores var authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri, - new[] {Permission.CanModifyStoreSettings, Permission.CanModifyServerSettings}).ToString(); + new[] { Permission.CanModifyStoreSettings, Permission.CanModifyServerSettings }).ToString(); s.Driver.Navigate().GoToUrl(authUrl); s.Driver.PageSource.Contains("kukksappname"); Assert.Equal("hidden", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("type").ToLowerInvariant()); Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("value").ToLowerInvariant()); Assert.Equal("hidden", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("type").ToLowerInvariant()); - Assert.Equal("true",s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("value").ToLowerInvariant()); + Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("value").ToLowerInvariant()); Assert.DoesNotContain("change-store-mode", s.Driver.PageSource); s.Driver.FindElement(By.Id("consent-yes")).Click(); var url = s.Driver.Url; @@ -129,20 +131,20 @@ namespace BTCPayServer.Tests .Select(s1 => new KeyValuePair(s1.Split("=")[0], s1.Split("=")[1])); var apiKeyRepo = s.Server.PayTester.GetService(); - + await TestApiAgainstAccessToken(results.Single(pair => pair.Key == "key").Value, tester, user, (await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).Permissions); authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri, - new[] {Permission.CanModifyStoreSettings, Permission.CanModifyServerSettings}, false, true).ToString(); - + new[] { Permission.CanModifyStoreSettings, Permission.CanModifyServerSettings }, false, true).ToString(); + s.Driver.Navigate().GoToUrl(authUrl); Assert.DoesNotContain("kukksappname", s.Driver.PageSource); Assert.Equal("checkbox", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("type").ToLowerInvariant()); Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("value").ToLowerInvariant()); Assert.Equal("checkbox", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("type").ToLowerInvariant()); - Assert.Equal("true",s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("value").ToLowerInvariant()); + Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("value").ToLowerInvariant()); s.SetCheckbox(s, "btcpay.server.canmodifyserversettings", false); Assert.Contains("change-store-mode", s.Driver.PageSource); @@ -150,22 +152,38 @@ namespace BTCPayServer.Tests url = s.Driver.Url; results = url.Split("?").Last().Split("&") .Select(s1 => new KeyValuePair(s1.Split("=")[0], s1.Split("=")[1])); - + await TestApiAgainstAccessToken(results.Single(pair => pair.Key == "key").Value, tester, user, (await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).Permissions); - + } } async Task TestApiAgainstAccessToken(string accessToken, ServerTester tester, TestAccount testAccount, - string permissionFormatted) + string expectedPermissionsString) { - var permissions = Permission.ToPermissions(permissionFormatted); - var resultUser = - await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/id", - tester.PayTester.HttpClient); - Assert.Equal(testAccount.UserId, resultUser); + var expectedPermissions = Permission.ToPermissions(expectedPermissionsString).ToArray(); + expectedPermissions ??= new Permission[0]; + var apikeydata = await TestApiAgainstAccessToken(accessToken, $"api/v1/api-keys/current", tester.PayTester.HttpClient); + var permissions = Permission.ToPermissions(apikeydata.Permissions).ToArray(); + Assert.Equal(expectedPermissions.Length, permissions.Length); + foreach (var expectPermission in expectedPermissions) + { + Assert.True(permissions.Any(p => p == expectPermission), $"Missing expected permission {expectPermission}"); + } + if (permissions.Contains(Permission.Create(Permission.CanViewProfile))) + { + var resultUser = await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/id", tester.PayTester.HttpClient); + Assert.Equal(testAccount.UserId, resultUser); + } + else + { + await Assert.ThrowsAnyAsync(async () => + { + await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/id", tester.PayTester.HttpClient); + }); + } //create a second user to see if any of its data gets messed upin our results. var secondUser = tester.NewAccount(); secondUser.GrantAccess(); @@ -212,7 +230,7 @@ namespace BTCPayServer.Tests data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase)); shouldBeAuthorized = true; } - + if (!shouldBeAuthorized) { await Assert.ThrowsAnyAsync(async () => @@ -231,9 +249,9 @@ namespace BTCPayServer.Tests data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase)); } } - else if(!permissions.Contains(unrestricted)) + else if (!permissions.Contains(unrestricted)) { - + await Assert.ThrowsAnyAsync(async () => { await TestApiAgainstAccessToken(accessToken, From 318d8266945f15e83c44736302deb653f7783b56 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 20 Mar 2020 13:41:47 +0900 Subject: [PATCH 298/810] Rename Permissions.Can.. to Policies.Can.. --- BTCPayServer.Client/Permissions.cs | 44 ++++++++++--------- BTCPayServer.Tests/ApiKeysTests.cs | 26 +++++------ BTCPayServer.Tests/GreenfieldAPITests.cs | 14 +++--- BTCPayServer.Tests/UnitTest1.cs | 18 ++++---- .../Controllers/InvoiceController.API.cs | 2 +- .../Controllers/InvoiceController.UI.cs | 2 +- .../RestApi/TestApiKeyController.cs | 12 ++--- .../RestApi/Users/UsersController.cs | 2 +- BTCPayServer/Controllers/ServerController.cs | 2 +- BTCPayServer/Controllers/StoresController.cs | 2 +- BTCPayServer/Controllers/WalletsController.cs | 6 +-- .../{Policies.cs => ServerPolicies.cs} | 4 +- .../Monero/UI/MoneroLikeStoreController.cs | 4 +- 13 files changed, 70 insertions(+), 68 deletions(-) rename BTCPayServer/Security/{Policies.cs => ServerPolicies.cs} (88%) diff --git a/BTCPayServer.Client/Permissions.cs b/BTCPayServer.Client/Permissions.cs index de225cfc2..1dfa845c0 100644 --- a/BTCPayServer.Client/Permissions.cs +++ b/BTCPayServer.Client/Permissions.cs @@ -5,7 +5,7 @@ using System.Text.Json.Serialization; namespace BTCPayServer.Client { - public class Permission + public class Policies { public const string CanModifyServerSettings = "btcpay.server.canmodifyserversettings"; public const string CanModifyStoreSettings = "btcpay.store.canmodifystoresettings"; @@ -15,7 +15,6 @@ namespace BTCPayServer.Client public const string CanViewProfile = "btcpay.user.canviewprofile"; public const string CanCreateUser = "btcpay.server.cancreateuser"; public const string Unrestricted = "unrestricted"; - public static IEnumerable AllPolicies { get @@ -30,7 +29,18 @@ namespace BTCPayServer.Client yield return Unrestricted; } } + public static bool IsValidPolicy(string policy) + { + return AllPolicies.Any(p => p.Equals(policy, StringComparison.OrdinalIgnoreCase)); + } + public static bool IsStorePolicy(string policy) + { + return policy.StartsWith("btcpay.store", StringComparison.OrdinalIgnoreCase); + } + } + public class Permission + { public static Permission Create(string policy, string storeId = null) { if (TryCreatePermission(policy, storeId, out var r)) @@ -44,9 +54,9 @@ namespace BTCPayServer.Client if (policy == null) throw new ArgumentNullException(nameof(policy)); policy = policy.Trim().ToLowerInvariant(); - if (!IsValidPolicy(policy)) + if (!Policies.IsValidPolicy(policy)) return false; - if (storeId != null && !IsStorePolicy(policy)) + if (storeId != null && !Policies.IsStorePolicy(policy)) return false; permission = new Permission(policy, storeId); return true; @@ -62,7 +72,7 @@ namespace BTCPayServer.Client if (separator == -1) { str = str.ToLowerInvariant(); - if (!IsValidPolicy(str)) + if (!Policies.IsValidPolicy(str)) return false; permission = new Permission(str, null); return true; @@ -70,9 +80,9 @@ namespace BTCPayServer.Client else { var policy = str.Substring(0, separator).ToLowerInvariant(); - if (!IsValidPolicy(policy)) + if (!Policies.IsValidPolicy(policy)) return false; - if (!IsStorePolicy(policy)) + if (!Policies.IsStorePolicy(policy)) return false; var storeId = str.Substring(separator + 1); if (storeId.Length == 0) @@ -82,15 +92,7 @@ namespace BTCPayServer.Client } } - private static bool IsValidPolicy(string policy) - { - return AllPolicies.Any(p => p.Equals(policy, StringComparison.OrdinalIgnoreCase)); - } - - private static bool IsStorePolicy(string policy) - { - return policy.StartsWith("btcpay.store", StringComparison.OrdinalIgnoreCase); - } + internal Permission(string policy, string storeId) { @@ -107,7 +109,7 @@ namespace BTCPayServer.Client { return false; } - if (!IsStorePolicy(subpermission.Policy)) + if (!Policies.IsStorePolicy(subpermission.Policy)) return true; return StoreId == null || subpermission.StoreId == this.StoreId; } @@ -133,15 +135,15 @@ namespace BTCPayServer.Client private bool ContainsPolicy(string subpolicy) { - if (this.Policy == Unrestricted) + if (this.Policy == Policies.Unrestricted) return true; if (this.Policy == subpolicy) return true; - if (subpolicy == CanViewStoreSettings && this.Policy == CanModifyStoreSettings) + if (subpolicy == Policies.CanViewStoreSettings && this.Policy == Policies.CanModifyStoreSettings) return true; - if (subpolicy == CanCreateInvoice && this.Policy == CanModifyStoreSettings) + if (subpolicy == Policies.CanCreateInvoice && this.Policy == Policies.CanModifyStoreSettings) return true; - if (subpolicy == CanViewProfile && this.Policy == CanModifyProfile) + if (subpolicy == Policies.CanViewProfile && this.Policy == Policies.CanModifyProfile) return true; return false; } diff --git a/BTCPayServer.Tests/ApiKeysTests.cs b/BTCPayServer.Tests/ApiKeysTests.cs index 322ccd31d..ee7c64f4d 100644 --- a/BTCPayServer.Tests/ApiKeysTests.cs +++ b/BTCPayServer.Tests/ApiKeysTests.cs @@ -67,7 +67,7 @@ namespace BTCPayServer.Tests var superApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; //this api key has access to everything - await TestApiAgainstAccessToken(superApiKey, tester, user, $"{Permission.CanModifyServerSettings};{Permission.CanModifyStoreSettings};{Permission.CanViewProfile}"); + await TestApiAgainstAccessToken(superApiKey, tester, user, $"{Policies.CanModifyServerSettings};{Policies.CanModifyStoreSettings};{Policies.CanViewProfile}"); s.Driver.FindElement(By.Id("AddApiKey")).Click(); @@ -75,7 +75,7 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("Generate")).Click(); var serverOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user, - Permission.CanModifyServerSettings); + Policies.CanModifyServerSettings); s.Driver.FindElement(By.Id("AddApiKey")).Click(); @@ -83,7 +83,7 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("Generate")).Click(); var allStoreOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; await TestApiAgainstAccessToken(allStoreOnlyApiKey, tester, user, - Permission.CanModifyStoreSettings); + Policies.CanModifyStoreSettings); s.Driver.FindElement(By.Id("AddApiKey")).Click(); s.Driver.FindElement(By.CssSelector("button[value=change-store-mode]")).Click(); @@ -95,7 +95,7 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("Generate")).Click(); var selectiveStoreApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; await TestApiAgainstAccessToken(selectiveStoreApiKey, tester, user, - Permission.Create(Permission.CanModifyStoreSettings, storeId).ToString()); + Permission.Create(Policies.CanModifyStoreSettings, storeId).ToString()); s.Driver.FindElement(By.Id("AddApiKey")).Click(); s.Driver.FindElement(By.Id("Generate")).Click(); @@ -117,7 +117,7 @@ namespace BTCPayServer.Tests //strict //selectiveStores var authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri, - new[] { Permission.CanModifyStoreSettings, Permission.CanModifyServerSettings }).ToString(); + new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }).ToString(); s.Driver.Navigate().GoToUrl(authUrl); s.Driver.PageSource.Contains("kukksappname"); Assert.Equal("hidden", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("type").ToLowerInvariant()); @@ -136,7 +136,7 @@ namespace BTCPayServer.Tests (await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).Permissions); authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri, - new[] { Permission.CanModifyStoreSettings, Permission.CanModifyServerSettings }, false, true).ToString(); + new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true).ToString(); s.Driver.Navigate().GoToUrl(authUrl); Assert.DoesNotContain("kukksappname", s.Driver.PageSource); @@ -172,7 +172,7 @@ namespace BTCPayServer.Tests Assert.True(permissions.Any(p => p == expectPermission), $"Missing expected permission {expectPermission}"); } - if (permissions.Contains(Permission.Create(Permission.CanViewProfile))) + if (permissions.Contains(Permission.Create(Policies.CanViewProfile))) { var resultUser = await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/id", tester.PayTester.HttpClient); Assert.Equal(testAccount.UserId, resultUser); @@ -188,10 +188,10 @@ namespace BTCPayServer.Tests var secondUser = tester.NewAccount(); secondUser.GrantAccess(); - var canModifyAllStores = Permission.Create(Permission.CanModifyStoreSettings, null); - var canModifyServer = Permission.Create(Permission.CanModifyServerSettings, null); - var unrestricted = Permission.Create(Permission.Unrestricted, null); - var selectiveStorePermissions = permissions.Where(p => p.StoreId != null && p.Policy == Permission.CanModifyStoreSettings); + var canModifyAllStores = Permission.Create(Policies.CanModifyStoreSettings, null); + var canModifyServer = Permission.Create(Policies.CanModifyServerSettings, null); + var unrestricted = Permission.Create(Policies.Unrestricted, null); + var selectiveStorePermissions = permissions.Where(p => p.StoreId != null && p.Policy == Policies.CanModifyStoreSettings); if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Any()) { var resultStores = @@ -209,7 +209,7 @@ namespace BTCPayServer.Tests } bool shouldBeAuthorized = false; - if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Contains(Permission.Create(Permission.CanViewStoreSettings, testAccount.StoreId))) + if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Contains(Permission.Create(Policies.CanViewStoreSettings, testAccount.StoreId))) { Assert.True(await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/stores/{testAccount.StoreId}/can-view", @@ -218,7 +218,7 @@ namespace BTCPayServer.Tests data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase)); shouldBeAuthorized = true; } - if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Contains(Permission.Create(Permission.CanModifyStoreSettings, testAccount.StoreId))) + if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Contains(Permission.Create(Policies.CanModifyStoreSettings, testAccount.StoreId))) { Assert.True(await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/stores/{testAccount.StoreId}/can-view", diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index c0aea9463..b3569dcbe 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -38,7 +38,7 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); user.GrantAccess(); await user.MakeAdmin(); - var client = await user.CreateClient(Permission.CanModifyServerSettings, Permission.CanModifyStoreSettings); + var client = await user.CreateClient(Policies.CanModifyServerSettings, Policies.CanModifyStoreSettings); //Get current api key var apiKeyData = await client.GetCurrentAPIKeyInfo(); Assert.NotNull(apiKeyData); @@ -97,14 +97,14 @@ namespace BTCPayServer.Tests var adminAcc = tester.NewAccount(); adminAcc.UserId = admin.Id; adminAcc.IsAdmin = true; - var adminClient = await adminAcc.CreateClient(Permission.CanModifyProfile); + var adminClient = await adminAcc.CreateClient(Policies.CanModifyProfile); // We should be forbidden to create a new user without proper admin permissions await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" })); await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true })); // However, should be ok with the server management permissions - adminClient = await adminAcc.CreateClient(Permission.CanModifyServerSettings); + adminClient = await adminAcc.CreateClient(Policies.CanModifyServerSettings); await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" }); // Even creating new admin should be ok await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true }); @@ -112,7 +112,7 @@ namespace BTCPayServer.Tests var user1Acc = tester.NewAccount(); user1Acc.UserId = user1.Id; user1Acc.IsAdmin = false; - var user1Client = await user1Acc.CreateClient(Permission.CanModifyServerSettings); + var user1Client = await user1Acc.CreateClient(Policies.CanModifyServerSettings); // User1 trying to get server management would still fail to create user await AssertHttpError(403, async () => await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" })); @@ -141,9 +141,9 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); user.GrantAccess(); await user.MakeAdmin(); - var clientProfile = await user.CreateClient(Permission.CanModifyProfile); - var clientServer = await user.CreateClient(Permission.CanModifyServerSettings, Permission.CanViewProfile); - var clientInsufficient = await user.CreateClient(Permission.CanModifyStoreSettings); + var clientProfile = await user.CreateClient(Policies.CanModifyProfile); + var clientServer = await user.CreateClient(Policies.CanModifyServerSettings, Policies.CanViewProfile); + var clientInsufficient = await user.CreateClient(Policies.CanModifyStoreSettings); var apiKeyProfileUserData = await clientProfile.GetCurrentUser(); diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 94013f5d0..e43973286 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -3005,16 +3005,16 @@ noninventoryitem: [Trait("Fast", "Fast")] public void CanUsePermission() { - Assert.True(Permission.Create(Permission.CanModifyServerSettings).Contains(Permission.Create(Permission.CanModifyServerSettings))); - Assert.True(Permission.Create(Permission.CanModifyProfile).Contains(Permission.Create(Permission.CanViewProfile))); - Assert.True(Permission.Create(Permission.CanModifyStoreSettings).Contains(Permission.Create(Permission.CanViewStoreSettings))); - Assert.False(Permission.Create(Permission.CanViewStoreSettings).Contains(Permission.Create(Permission.CanModifyStoreSettings))); - Assert.False(Permission.Create(Permission.CanModifyServerSettings).Contains(Permission.Create(Permission.CanModifyStoreSettings))); - Assert.True(Permission.Create(Permission.Unrestricted).Contains(Permission.Create(Permission.CanModifyStoreSettings))); - Assert.True(Permission.Create(Permission.Unrestricted).Contains(Permission.Create(Permission.CanModifyStoreSettings, "abc"))); + Assert.True(Permission.Create(Policies.CanModifyServerSettings).Contains(Permission.Create(Policies.CanModifyServerSettings))); + Assert.True(Permission.Create(Policies.CanModifyProfile).Contains(Permission.Create(Policies.CanViewProfile))); + Assert.True(Permission.Create(Policies.CanModifyStoreSettings).Contains(Permission.Create(Policies.CanViewStoreSettings))); + Assert.False(Permission.Create(Policies.CanViewStoreSettings).Contains(Permission.Create(Policies.CanModifyStoreSettings))); + Assert.False(Permission.Create(Policies.CanModifyServerSettings).Contains(Permission.Create(Policies.CanModifyStoreSettings))); + Assert.True(Permission.Create(Policies.Unrestricted).Contains(Permission.Create(Policies.CanModifyStoreSettings))); + Assert.True(Permission.Create(Policies.Unrestricted).Contains(Permission.Create(Policies.CanModifyStoreSettings, "abc"))); - Assert.True(Permission.Create(Permission.CanViewStoreSettings).Contains(Permission.Create(Permission.CanViewStoreSettings, "abcd"))); - Assert.False(Permission.Create(Permission.CanModifyStoreSettings, "abcd").Contains(Permission.Create(Permission.CanModifyStoreSettings))); + Assert.True(Permission.Create(Policies.CanViewStoreSettings).Contains(Permission.Create(Policies.CanViewStoreSettings, "abcd"))); + Assert.False(Permission.Create(Policies.CanModifyStoreSettings, "abcd").Contains(Permission.Create(Policies.CanModifyStoreSettings))); } [Fact(Timeout = TestTimeout)] diff --git a/BTCPayServer/Controllers/InvoiceController.API.cs b/BTCPayServer/Controllers/InvoiceController.API.cs index e95a11c17..16136cd6a 100644 --- a/BTCPayServer/Controllers/InvoiceController.API.cs +++ b/BTCPayServer/Controllers/InvoiceController.API.cs @@ -13,7 +13,7 @@ using Microsoft.AspNetCore.Mvc; namespace BTCPayServer.Controllers { [BitpayAPIConstraint] - [Authorize(Permission.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Bitpay)] + [Authorize(Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Bitpay)] public class InvoiceControllerAPI : Controller { private InvoiceController _InvoiceController; diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 9eca43d5a..f057420fb 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -511,7 +511,7 @@ namespace BTCPayServer.Controllers [HttpPost] [Route("invoices/create")] - [Authorize(Policy = Permission.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [BitpayAPIConstraint(false)] public async Task CreateInvoice(CreateInvoiceModel model, CancellationToken cancellationToken) { diff --git a/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs b/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs index ac81ef575..971fd63b1 100644 --- a/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs +++ b/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs @@ -28,35 +28,35 @@ namespace BTCPayServer.Controllers.RestApi } [HttpGet("me/id")] - [Authorize(Policy = Permission.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] public string GetCurrentUserId() { return _userManager.GetUserId(User); } [HttpGet("me")] - [Authorize(Policy = Permission.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] public async Task GetCurrentUser() { return await _userManager.GetUserAsync(User); } [HttpGet("me/is-admin")] - [Authorize(Policy = Permission.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] public bool AmIAnAdmin() { return true; } [HttpGet("me/stores")] - [Authorize(Policy = Permission.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] public StoreData[] GetCurrentUserStores() { return this.HttpContext.GetStoresData(); } [HttpGet("me/stores/{storeId}/can-view")] - [Authorize(Policy = Permission.CanViewStoreSettings, + [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] public bool CanViewStore(string storeId) { @@ -64,7 +64,7 @@ namespace BTCPayServer.Controllers.RestApi } [HttpGet("me/stores/{storeId}/can-edit")] - [Authorize(Policy = Permission.CanModifyStoreSettings, + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] public bool CanEditStore(string storeId) { diff --git a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs index 7c2763d32..883769264 100644 --- a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs +++ b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs @@ -55,7 +55,7 @@ namespace BTCPayServer.Controllers.RestApi.Users _authorizationService = authorizationService; } - [Authorize(Policy = Permission.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] [HttpGet("~/api/v1/users/me")] public async Task> GetCurrentUser() { diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index 28b7df6cb..43271d173 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -39,7 +39,7 @@ using BTCPayServer.Client; namespace BTCPayServer.Controllers { - [Authorize(Policy = Permission.CanModifyServerSettings, + [Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = BTCPayServer.Security.AuthenticationSchemes.Cookie)] public partial class ServerController : Controller { diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index fcce5c962..2f22c1325 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -34,7 +34,7 @@ namespace BTCPayServer.Controllers { [Route("stores")] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] - [Authorize(Policy = Permission.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [AutoValidateAntiforgeryToken] public partial class StoresController : Controller { diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index bce2f827f..b363aebaa 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -31,7 +31,7 @@ using Newtonsoft.Json; namespace BTCPayServer.Controllers { [Route("wallets")] - [Authorize(Policy = Permission.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [AutoValidateAntiforgeryToken] public partial class WalletsController : Controller { @@ -840,7 +840,7 @@ namespace BTCPayServer.Controllers var vm = new RescanWalletModel(); vm.IsFullySync = _dashboard.IsFullySynched(walletId.CryptoCode, out var unused); - vm.IsServerAdmin = (await _authorizationService.AuthorizeAsync(User, Permission.CanModifyServerSettings)).Succeeded; + vm.IsServerAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings)).Succeeded; vm.IsSupportedByCurrency = _dashboard.Get(walletId.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanScanTxoutSet == true; var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode); var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.AccountDerivation); @@ -870,7 +870,7 @@ namespace BTCPayServer.Controllers [HttpPost] [Route("{walletId}/rescan")] - [Authorize(Policy = Permission.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] public async Task WalletRescan( [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, RescanWalletModel vm) diff --git a/BTCPayServer/Security/Policies.cs b/BTCPayServer/Security/ServerPolicies.cs similarity index 88% rename from BTCPayServer/Security/Policies.cs rename to BTCPayServer/Security/ServerPolicies.cs index b4cfccc5a..6028de19f 100644 --- a/BTCPayServer/Security/Policies.cs +++ b/BTCPayServer/Security/ServerPolicies.cs @@ -3,11 +3,11 @@ using Microsoft.AspNetCore.Authorization; namespace BTCPayServer.Security { - public static class Policies + public static class ServerPolicies { public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options) { - foreach (var p in Permission.AllPolicies) + foreach (var p in Policies.AllPolicies) { options.AddPolicy(p); } diff --git a/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs b/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs index 7cccf57fa..87f852063 100644 --- a/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs +++ b/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs @@ -27,8 +27,8 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI [Route("stores/{storeId}/monerolike")] [OnlyIfSupportAttribute("XMR")] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] - [Authorize(Policy = Permission.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - [Authorize(Policy = Permission.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] public class MoneroLikeStoreController : Controller { private readonly MoneroLikeConfiguration _MoneroLikeConfiguration; From 0bfc12ae3d89843882e883a2651e8f14e9125dae Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 20 Mar 2020 13:44:02 +0900 Subject: [PATCH 299/810] Fix build --- BTCPayServer/Controllers/RateController.cs | 2 +- BTCPayServer/Controllers/RestApi/Users/UsersController.cs | 4 ++-- BTCPayServer/Controllers/ServerController.cs | 2 +- BTCPayServer/Controllers/WalletsController.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/BTCPayServer/Controllers/RateController.cs b/BTCPayServer/Controllers/RateController.cs index 67249bd0b..aeaac8fd6 100644 --- a/BTCPayServer/Controllers/RateController.cs +++ b/BTCPayServer/Controllers/RateController.cs @@ -20,7 +20,7 @@ using BTCPayServer.Security.Bitpay; namespace BTCPayServer.Controllers { [EnableCors(CorsPolicies.All)] - [Authorize(Policy = Policies.CanGetRates.Key, AuthenticationSchemes = Security.AuthenticationSchemes.Bitpay)] + [Authorize(Policy = ServerPolicies.CanGetRates.Key, AuthenticationSchemes = Security.AuthenticationSchemes.Bitpay)] public class RateController : Controller { public StoreData CurrentStore diff --git a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs index 883769264..6a7a7aca2 100644 --- a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs +++ b/BTCPayServer/Controllers/RestApi/Users/UsersController.cs @@ -87,7 +87,7 @@ namespace BTCPayServer.Controllers.RestApi.Users if (anyAdmin && request.IsAdministrator is true && !isAuth) return Forbid(AuthenticationSchemes.ApiKey); // You are de-facto admin if there is no other admin, else you need to be auth and pass policy requirements - bool isAdmin = anyAdmin ? (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Permission.CanModifyServerSettings))).Succeeded + bool isAdmin = anyAdmin ? (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanModifyServerSettings))).Succeeded && isAuth : true; // You need to be admin to create an admin @@ -97,7 +97,7 @@ namespace BTCPayServer.Controllers.RestApi.Users if (!isAdmin && policies.LockSubscription) { // If we are not admin and subscriptions are locked, we need to check the Policies.CanCreateUser.Key permission - var canCreateUser = (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Permission.CanCreateUser))).Succeeded; + var canCreateUser = (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanCreateUser))).Succeeded; if (!isAuth || !canCreateUser) return Forbid(AuthenticationSchemes.ApiKey); } diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index 43271d173..a2f7ddd09 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -39,7 +39,7 @@ using BTCPayServer.Client; namespace BTCPayServer.Controllers { - [Authorize(Policy = Policies.CanModifyServerSettings, + [Authorize(Policy = BTCPayServer.Client.Policies.CanModifyServerSettings, AuthenticationSchemes = BTCPayServer.Security.AuthenticationSchemes.Cookie)] public partial class ServerController : Controller { diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index b363aebaa..5eaae18f0 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -367,7 +367,7 @@ namespace BTCPayServer.Controllers private async Task CanUseHotWallet() { - var isAdmin = (await _authorizationService.AuthorizeAsync(User, Permission.CanModifyServerSettings)).Succeeded; + var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings)).Succeeded; if (isAdmin) return true; var policies = await _settingsRepository.GetSettingAsync(); From 2def9e7bd38d8e237cbbadb3bcf868011d105e1c Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 20 Mar 2020 13:58:07 +0900 Subject: [PATCH 300/810] fix build --- .../Controllers/ManageController.APIKeys.cs | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/BTCPayServer/Controllers/ManageController.APIKeys.cs b/BTCPayServer/Controllers/ManageController.APIKeys.cs index 65c9c92bb..8a2429ed6 100644 --- a/BTCPayServer/Controllers/ManageController.APIKeys.cs +++ b/BTCPayServer/Controllers/ManageController.APIKeys.cs @@ -130,7 +130,7 @@ namespace BTCPayServer.Controllers } var permissions = Permission.ToPermissions(viewModel.Permissions).ToHashSet(); - if (permissions.Contains(Permission.Create(Permission.CanModifyStoreSettings))) + if (permissions.Contains(Permission.Create(Policies.CanModifyStoreSettings))) { if (!viewModel.SelectiveStores && viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific) @@ -253,11 +253,11 @@ namespace BTCPayServer.Controllers } if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.AllStores && viewModel.StoreManagementPermission.Value) { - permissions.Add(Permission.Create(Permission.CanModifyStoreSettings)); + permissions.Add(Permission.Create(Policies.CanModifyStoreSettings)); } else if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific) { - permissions.AddRange(viewModel.SpecificStores.Select(s => Permission.Create(Permission.CanModifyStoreSettings, s))); + permissions.AddRange(viewModel.SpecificStores.Select(s => Permission.Create(Policies.CanModifyStoreSettings, s))); } return permissions.Distinct(); } @@ -265,15 +265,15 @@ namespace BTCPayServer.Controllers private async Task SetViewModelValues(T viewModel) where T : AddApiKeyViewModel { viewModel.Stores = await _StoreRepository.GetStoresByUserId(_userManager.GetUserId(User)); - var isAdmin = (await _authorizationService.AuthorizeAsync(User, Permission.CanModifyServerSettings)).Succeeded; - viewModel.PermissionValues ??= Permission.AllPolicies.Where(p => p != Permission.CanModifyStoreSettings) + var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings)).Succeeded; + viewModel.PermissionValues ??= Policies.AllPolicies.Where(p => p != Policies.CanModifyStoreSettings) .Select(s => new AddApiKeyViewModel.PermissionValueItem() { Permission = s, Value = false }).ToList(); if (!isAdmin) { foreach (var p in viewModel.PermissionValues) { - if (p.Permission == Permission.CanCreateUser || - p.Permission == Permission.CanModifyServerSettings) + if (p.Permission == Policies.CanCreateUser || + p.Permission == Policies.CanModifyServerSettings) { p.Forbidden = true; } @@ -288,19 +288,19 @@ namespace BTCPayServer.Controllers { StoreManagementPermission = new PermissionValueItem() { - Permission = Permission.CanModifyStoreSettings, + Permission = Policies.CanModifyStoreSettings, Value = false }; StoreManagementSelectivePermission = new PermissionValueItem() { - Permission = $"{Permission.CanModifyStoreSettings}:", + Permission = $"{Policies.CanModifyStoreSettings}:", Value = true }; } public AddApiKeyViewModel(IEnumerable permissions):this() { - StoreManagementPermission.Value = permissions.Any(p => p.Policy == Permission.CanModifyStoreSettings && p.StoreId == null); - PermissionValues = permissions.Where(p => p.Policy != Permission.CanModifyStoreSettings) + StoreManagementPermission.Value = permissions.Any(p => p.Policy == Policies.CanModifyStoreSettings && p.StoreId == null); + PermissionValues = permissions.Where(p => p.Policy != Policies.CanModifyStoreSettings) .Select(p => new PermissionValueItem() { Permission = p.ToString(), Value = true }) .ToList(); } @@ -318,13 +318,13 @@ namespace BTCPayServer.Controllers if (this.StoreMode == ApiKeyStoreMode.AllStores) { if (StoreManagementPermission.Value) - yield return Permission.Create(Permission.CanModifyStoreSettings); + yield return Permission.Create(Policies.CanModifyStoreSettings); } else if (this.StoreMode == ApiKeyStoreMode.Specific && SpecificStores is List) { foreach (var p in SpecificStores) { - if (Permission.TryCreatePermission(Permission.CanModifyStoreSettings, p, out var pp)) + if (Permission.TryCreatePermission(Policies.CanModifyStoreSettings, p, out var pp)) yield return pp; } } @@ -349,15 +349,15 @@ namespace BTCPayServer.Controllers { public static readonly Dictionary PermissionDescriptions = new Dictionary() { - {BTCPayServer.Client.Permission.Unrestricted, ("Unrestricted access", "The app will have unrestricted access to your account.")}, - {BTCPayServer.Client.Permission.CanCreateUser, ("Create new users", "The app will be able to create new users on this server.")}, - {BTCPayServer.Client.Permission.CanModifyStoreSettings, ("Modify your stores", "The app will be able to create, view and modify, delete and create new invoices on the all your stores.")}, - {BTCPayServer.Client.Permission.CanViewStoreSettings, ("View your stores", "The app will be able to create, view all your stores.")}, - {$"{BTCPayServer.Client.Permission.CanModifyStoreSettings}:", ("Manage selected stores", "The app will be able to view, modify, delete and create new invoices on the selected stores.")}, - {BTCPayServer.Client.Permission.CanModifyServerSettings, ("Manage your server", "The app will have total control on your server")}, - {BTCPayServer.Client.Permission.CanViewProfile, ("View your profile", "The app will be able to view your user profile.")}, - {BTCPayServer.Client.Permission.CanModifyProfile, ("Manage your profile", "The app will be able to view and modify your user profile.")}, - {BTCPayServer.Client.Permission.CanCreateInvoice, ("Create an invoice", "The app will be able to create new invoice.")}, + {BTCPayServer.Client.Policies.Unrestricted, ("Unrestricted access", "The app will have unrestricted access to your account.")}, + {BTCPayServer.Client.Policies.CanCreateUser, ("Create new users", "The app will be able to create new users on this server.")}, + {BTCPayServer.Client.Policies.CanModifyStoreSettings, ("Modify your stores", "The app will be able to create, view and modify, delete and create new invoices on the all your stores.")}, + {BTCPayServer.Client.Policies.CanViewStoreSettings, ("View your stores", "The app will be able to create, view all your stores.")}, + {$"{BTCPayServer.Client.Policies.CanModifyStoreSettings}:", ("Manage selected stores", "The app will be able to view, modify, delete and create new invoices on the selected stores.")}, + {BTCPayServer.Client.Policies.CanModifyServerSettings, ("Manage your server", "The app will have total control on your server")}, + {BTCPayServer.Client.Policies.CanViewProfile, ("View your profile", "The app will be able to view your user profile.")}, + {BTCPayServer.Client.Policies.CanModifyProfile, ("Manage your profile", "The app will be able to view and modify your user profile.")}, + {BTCPayServer.Client.Policies.CanCreateInvoice, ("Create an invoice", "The app will be able to create new invoice.")}, }; public string Title { From a0e638d5004e40403a95f95a8ea8266341482819 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 20 Mar 2020 14:01:51 +0900 Subject: [PATCH 301/810] Switch from System.Text.Json to Newtonsoft, typify the BTCPayServer.Client --- .../BTCPayServer.Client.csproj | 3 +- BTCPayServer.Client/BTCPayServerClient.cs | 16 +++++---- .../JsonConverters/PermissionJsonConverter.cs | 34 +++++++++++++++++++ BTCPayServer.Client/Models/ApiKeyData.cs | 2 +- BTCPayServer.Client/Permissions.cs | 1 - BTCPayServer.Client/Serializer.cs | 18 ++++++++++ BTCPayServer.Tests/ApiKeysTests.cs | 2 +- .../Controllers/ManageController.APIKeys.cs | 1 - .../RestApi/ApiKeys/ApiKeysController.cs | 2 +- .../Controllers/StoresController.BTCLike.cs | 2 +- BTCPayServer/Controllers/VaultController.cs | 2 +- .../APIKeys/APIKeyAuthorizationHandler.cs | 12 +++---- .../Bitpay/BitpayAuthorizationHandler.cs | 4 +-- .../Security/CookieAuthorizationHandler.cs | 6 ++-- 14 files changed, 79 insertions(+), 26 deletions(-) create mode 100644 BTCPayServer.Client/JsonConverters/PermissionJsonConverter.cs create mode 100644 BTCPayServer.Client/Serializer.cs diff --git a/BTCPayServer.Client/BTCPayServer.Client.csproj b/BTCPayServer.Client/BTCPayServer.Client.csproj index 7522bd9e2..4acef7f94 100644 --- a/BTCPayServer.Client/BTCPayServer.Client.csproj +++ b/BTCPayServer.Client/BTCPayServer.Client.csproj @@ -5,7 +5,8 @@ - + + diff --git a/BTCPayServer.Client/BTCPayServerClient.cs b/BTCPayServer.Client/BTCPayServerClient.cs index 60c81c400..d3780411c 100644 --- a/BTCPayServer.Client/BTCPayServerClient.cs +++ b/BTCPayServer.Client/BTCPayServerClient.cs @@ -5,23 +5,25 @@ using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; -using System.Text.Json; using System.Threading.Tasks; +using Newtonsoft.Json; namespace BTCPayServer.Client { public partial class BTCPayServerClient { + static BTCPayServerClient() + { + _GlobalSerializer = new JsonSerializerSettings(); + Serializer.RegisterConverters(_GlobalSerializer); + } private readonly string _apiKey; private readonly Uri _btcpayHost; private readonly HttpClient _httpClient; public string APIKey => _apiKey; - private readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; + private static readonly JsonSerializerSettings _GlobalSerializer = new JsonSerializerSettings(); public BTCPayServerClient(Uri btcpayHost, HttpClient httpClient = null) { @@ -45,7 +47,7 @@ namespace BTCPayServer.Client protected async Task HandleResponse(HttpResponseMessage message) { HandleResponse(message); - return JsonSerializer.Deserialize(await message.Content.ReadAsStringAsync(), _serializerOptions); + return JsonConvert.DeserializeObject(await message.Content.ReadAsStringAsync(), _GlobalSerializer); } protected virtual HttpRequestMessage CreateHttpRequest(string path, @@ -73,7 +75,7 @@ namespace BTCPayServer.Client var request = CreateHttpRequest(path, queryPayload, method); if (typeof(T).IsPrimitive || !EqualityComparer.Default.Equals(bodyPayload, default(T))) { - request.Content = new StringContent(JsonSerializer.Serialize(bodyPayload, _serializerOptions), Encoding.UTF8, "application/json"); + request.Content = new StringContent(JsonConvert.SerializeObject(bodyPayload, _GlobalSerializer), Encoding.UTF8, "application/json"); } return request; diff --git a/BTCPayServer.Client/JsonConverters/PermissionJsonConverter.cs b/BTCPayServer.Client/JsonConverters/PermissionJsonConverter.cs new file mode 100644 index 000000000..26bf1eace --- /dev/null +++ b/BTCPayServer.Client/JsonConverters/PermissionJsonConverter.cs @@ -0,0 +1,34 @@ +using System; +using System.Reflection; +using System.Collections.Generic; +using System.Text; +using Newtonsoft.Json; +using NBitcoin.JsonConverters; + +namespace BTCPayServer.Client.JsonConverters +{ + public class PermissionJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return typeof(Permission).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + return null; + if (reader.TokenType != JsonToken.String) + throw new JsonObjectException("Type 'Permission' is expected to be a 'String'", reader); + if (reader.ReadAsString() is String s && Permission.TryParse(s, out var permission)) + return permission; + throw new JsonObjectException("Invalid 'Permission' String", reader); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value is Permission v) + writer.WriteValue(v.ToString()); + } + } +} diff --git a/BTCPayServer.Client/Models/ApiKeyData.cs b/BTCPayServer.Client/Models/ApiKeyData.cs index 58872cb11..244859f60 100644 --- a/BTCPayServer.Client/Models/ApiKeyData.cs +++ b/BTCPayServer.Client/Models/ApiKeyData.cs @@ -5,6 +5,6 @@ namespace BTCPayServer.Client.Models public string ApiKey { get; set; } public string Label { get; set; } public string UserId { get; set; } - public string[] Permissions { get; set; } + public Permission[] Permissions { get; set; } } } diff --git a/BTCPayServer.Client/Permissions.cs b/BTCPayServer.Client/Permissions.cs index 1dfa845c0..88adc5389 100644 --- a/BTCPayServer.Client/Permissions.cs +++ b/BTCPayServer.Client/Permissions.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.Json.Serialization; namespace BTCPayServer.Client { diff --git a/BTCPayServer.Client/Serializer.cs b/BTCPayServer.Client/Serializer.cs new file mode 100644 index 000000000..7ab7b43b4 --- /dev/null +++ b/BTCPayServer.Client/Serializer.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; +using BTCPayServer.Client.JsonConverters; +using Newtonsoft.Json; + +namespace BTCPayServer.Client +{ + public class Serializer + { + public static void RegisterConverters(JsonSerializerSettings settings) + { + if (settings == null) + throw new ArgumentNullException(nameof(settings)); + settings.Converters.Add(new PermissionJsonConverter()); + } + } +} diff --git a/BTCPayServer.Tests/ApiKeysTests.cs b/BTCPayServer.Tests/ApiKeysTests.cs index ee7c64f4d..fde2e5433 100644 --- a/BTCPayServer.Tests/ApiKeysTests.cs +++ b/BTCPayServer.Tests/ApiKeysTests.cs @@ -165,7 +165,7 @@ namespace BTCPayServer.Tests var expectedPermissions = Permission.ToPermissions(expectedPermissionsString).ToArray(); expectedPermissions ??= new Permission[0]; var apikeydata = await TestApiAgainstAccessToken(accessToken, $"api/v1/api-keys/current", tester.PayTester.HttpClient); - var permissions = Permission.ToPermissions(apikeydata.Permissions).ToArray(); + var permissions = apikeydata.Permissions; Assert.Equal(expectedPermissions.Length, permissions.Length); foreach (var expectPermission in expectedPermissions) { diff --git a/BTCPayServer/Controllers/ManageController.APIKeys.cs b/BTCPayServer/Controllers/ManageController.APIKeys.cs index 8a2429ed6..169a8b48e 100644 --- a/BTCPayServer/Controllers/ManageController.APIKeys.cs +++ b/BTCPayServer/Controllers/ManageController.APIKeys.cs @@ -331,7 +331,6 @@ namespace BTCPayServer.Controllers } public string Label { get; set; } public StoreData[] Stores { get; set; } - ApiKeyStoreMode _StoreMode; public ApiKeyStoreMode StoreMode { get; set; } public List SpecificStores { get; set; } = new List(); public PermissionValueItem StoreManagementPermission { get; set; } diff --git a/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs b/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs index cc7b7e7de..ea2dc4ff2 100644 --- a/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs +++ b/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs @@ -45,7 +45,7 @@ namespace BTCPayServer.Controllers.RestApi.ApiKeys { return new ApiKeyData() { - Permissions = Permission.ToPermissions(data.Permissions).Select(c => c.ToString()).ToArray(), + Permissions = Permission.ToPermissions(data.Permissions).ToArray(), ApiKey = data.Id, UserId = data.UserId, Label = data.Label diff --git a/BTCPayServer/Controllers/StoresController.BTCLike.cs b/BTCPayServer/Controllers/StoresController.BTCLike.cs index eebebf054..44d7d7370 100644 --- a/BTCPayServer/Controllers/StoresController.BTCLike.cs +++ b/BTCPayServer/Controllers/StoresController.BTCLike.cs @@ -387,7 +387,7 @@ namespace BTCPayServer.Controllers private async Task CanUseHotWallet() { - var isAdmin = (await _authorizationService.AuthorizeAsync(User, Permission.CanModifyServerSettings)).Succeeded; + var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings)).Succeeded; if (isAdmin) return true; var policies = await _settingsRepository.GetSettingAsync(); diff --git a/BTCPayServer/Controllers/VaultController.cs b/BTCPayServer/Controllers/VaultController.cs index edb4211a3..c07c59794 100644 --- a/BTCPayServer/Controllers/VaultController.cs +++ b/BTCPayServer/Controllers/VaultController.cs @@ -128,7 +128,7 @@ namespace BTCPayServer.Controllers } await websocketHelper.Send("{ \"info\": \"ready\"}", cancellationToken); o = JObject.Parse(await websocketHelper.NextMessageAsync(cancellationToken)); - var authorization = await _authorizationService.AuthorizeAsync(User, Permission.CanModifyStoreSettings); + var authorization = await _authorizationService.AuthorizeAsync(User, Policies.CanModifyStoreSettings); if (!authorization.Succeeded) { await websocketHelper.Send("{ \"error\": \"not-authorized\"}", cancellationToken); diff --git a/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs b/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs index 0c06c5760..a4306ff37 100644 --- a/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs +++ b/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs @@ -36,13 +36,13 @@ namespace BTCPayServer.Security.APIKeys bool success = false; switch (requirement.Policy) { - case Permission.CanModifyProfile: - case Permission.CanViewProfile: + case Policies.CanModifyProfile: + case Policies.CanViewProfile: success = context.HasPermission(Permission.Create(requirement.Policy)); break; - case Permission.CanViewStoreSettings: - case Permission.CanModifyStoreSettings: + case Policies.CanViewStoreSettings: + case Policies.CanModifyStoreSettings: var storeId = _HttpContext.GetImplicitStoreId(); var userid = _userManager.GetUserId(context.User); // Specific store action @@ -72,8 +72,8 @@ namespace BTCPayServer.Security.APIKeys success = true; } break; - case Permission.CanCreateUser: - case Permission.CanModifyServerSettings: + case Policies.CanCreateUser: + case Policies.CanModifyServerSettings: if (context.HasPermission(Permission.Create(requirement.Policy))) { var user = await _userManager.GetUserAsync(context.User); diff --git a/BTCPayServer/Security/Bitpay/BitpayAuthorizationHandler.cs b/BTCPayServer/Security/Bitpay/BitpayAuthorizationHandler.cs index 30b100e14..6183eeeab 100644 --- a/BTCPayServer/Security/Bitpay/BitpayAuthorizationHandler.cs +++ b/BTCPayServer/Security/Bitpay/BitpayAuthorizationHandler.cs @@ -55,7 +55,7 @@ namespace BTCPayServer.Security.Bitpay var anyoneCanInvoice = store.GetStoreBlob().AnyoneCanInvoice; switch (requirement.Policy) { - case Permission.CanCreateInvoice: + case Policies.CanCreateInvoice: if (!isAnonymous || (isAnonymous && anyoneCanInvoice)) { context.Succeed(requirement); @@ -63,7 +63,7 @@ namespace BTCPayServer.Security.Bitpay return; } break; - case Policies.CanGetRates.Key: + case ServerPolicies.CanGetRates.Key: context.Succeed(requirement); _HttpContext.SetStoreData(store); return; diff --git a/BTCPayServer/Security/CookieAuthorizationHandler.cs b/BTCPayServer/Security/CookieAuthorizationHandler.cs index 9f5c5e603..d2546dc22 100644 --- a/BTCPayServer/Security/CookieAuthorizationHandler.cs +++ b/BTCPayServer/Security/CookieAuthorizationHandler.cs @@ -36,7 +36,7 @@ namespace BTCPayServer.Security var isAdmin = context.User.IsInRole(Roles.ServerAdmin); switch (requirement.Policy) { - case Permission.CanModifyServerSettings: + case Policies.CanModifyServerSettings: if (isAdmin) context.Succeed(requirement); return; @@ -57,11 +57,11 @@ namespace BTCPayServer.Security bool success = false; switch (requirement.Policy) { - case Permission.CanModifyStoreSettings: + case Policies.CanModifyStoreSettings: if (store.Role == StoreRoles.Owner || isAdmin) success = true; break; - case Permission.CanCreateInvoice: + case Policies.CanCreateInvoice: if (store.Role == StoreRoles.Owner || store.Role == StoreRoles.Guest || isAdmin || From 55516a325385d18e2ee27d5f8defa2f535b457b2 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 20 Mar 2020 14:03:28 +0900 Subject: [PATCH 302/810] Remove excessive folders --- .../Controllers/RestApi/{ApiKeys => }/ApiKeysController.cs | 2 +- BTCPayServer/Controllers/RestApi/{Users => }/UsersController.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename BTCPayServer/Controllers/RestApi/{ApiKeys => }/ApiKeysController.cs (97%) rename BTCPayServer/Controllers/RestApi/{Users => }/UsersController.cs (99%) diff --git a/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs b/BTCPayServer/Controllers/RestApi/ApiKeysController.cs similarity index 97% rename from BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs rename to BTCPayServer/Controllers/RestApi/ApiKeysController.cs index ea2dc4ff2..d512552d7 100644 --- a/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs +++ b/BTCPayServer/Controllers/RestApi/ApiKeysController.cs @@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -namespace BTCPayServer.Controllers.RestApi.ApiKeys +namespace BTCPayServer.Controllers.RestApi { [ApiController] [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKey)] diff --git a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs b/BTCPayServer/Controllers/RestApi/UsersController.cs similarity index 99% rename from BTCPayServer/Controllers/RestApi/Users/UsersController.cs rename to BTCPayServer/Controllers/RestApi/UsersController.cs index 6a7a7aca2..9abefacee 100644 --- a/BTCPayServer/Controllers/RestApi/Users/UsersController.cs +++ b/BTCPayServer/Controllers/RestApi/UsersController.cs @@ -20,7 +20,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using NicolasDorier.RateLimits; using BTCPayServer.Client; -namespace BTCPayServer.Controllers.RestApi.Users +namespace BTCPayServer.Controllers.RestApi { [ApiController] [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKey)] From fb36ed2caed103f8d69a872969a78e6037f74816 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 20 Mar 2020 14:07:42 +0900 Subject: [PATCH 303/810] Fix tests --- BTCPayServer.Client/BTCPayServerClient.cs | 11 ++--------- BTCPayServer.Client/Serializer.cs | 15 +++++++++++++++ BTCPayServer.Tests/ApiKeysTests.cs | 2 +- BTCPayServer.Tests/GreenfieldAPITests.cs | 1 - 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/BTCPayServer.Client/BTCPayServerClient.cs b/BTCPayServer.Client/BTCPayServerClient.cs index d3780411c..4dfad35db 100644 --- a/BTCPayServer.Client/BTCPayServerClient.cs +++ b/BTCPayServer.Client/BTCPayServerClient.cs @@ -12,19 +12,12 @@ namespace BTCPayServer.Client { public partial class BTCPayServerClient { - static BTCPayServerClient() - { - _GlobalSerializer = new JsonSerializerSettings(); - Serializer.RegisterConverters(_GlobalSerializer); - } private readonly string _apiKey; private readonly Uri _btcpayHost; private readonly HttpClient _httpClient; public string APIKey => _apiKey; - private static readonly JsonSerializerSettings _GlobalSerializer = new JsonSerializerSettings(); - public BTCPayServerClient(Uri btcpayHost, HttpClient httpClient = null) { if (btcpayHost == null) @@ -47,7 +40,7 @@ namespace BTCPayServer.Client protected async Task HandleResponse(HttpResponseMessage message) { HandleResponse(message); - return JsonConvert.DeserializeObject(await message.Content.ReadAsStringAsync(), _GlobalSerializer); + return JsonConvert.DeserializeObject(await message.Content.ReadAsStringAsync(), Serializer.GlobalSerializerSettings); } protected virtual HttpRequestMessage CreateHttpRequest(string path, @@ -75,7 +68,7 @@ namespace BTCPayServer.Client var request = CreateHttpRequest(path, queryPayload, method); if (typeof(T).IsPrimitive || !EqualityComparer.Default.Equals(bodyPayload, default(T))) { - request.Content = new StringContent(JsonConvert.SerializeObject(bodyPayload, _GlobalSerializer), Encoding.UTF8, "application/json"); + request.Content = new StringContent(JsonConvert.SerializeObject(bodyPayload, Serializer.GlobalSerializerSettings), Encoding.UTF8, "application/json"); } return request; diff --git a/BTCPayServer.Client/Serializer.cs b/BTCPayServer.Client/Serializer.cs index 7ab7b43b4..91803026e 100644 --- a/BTCPayServer.Client/Serializer.cs +++ b/BTCPayServer.Client/Serializer.cs @@ -8,6 +8,21 @@ namespace BTCPayServer.Client { public class Serializer { + + private static JsonSerializerSettings _GlobalSerializerSettings; + public static JsonSerializerSettings GlobalSerializerSettings + { + get + { + if (_GlobalSerializerSettings is null) + { + var serializer = new JsonSerializerSettings(); + RegisterConverters(serializer); + _GlobalSerializerSettings = serializer; + } + return _GlobalSerializerSettings; + } + } public static void RegisterConverters(JsonSerializerSettings settings) { if (settings == null) diff --git a/BTCPayServer.Tests/ApiKeysTests.cs b/BTCPayServer.Tests/ApiKeysTests.cs index fde2e5433..5dcc68dc9 100644 --- a/BTCPayServer.Tests/ApiKeysTests.cs +++ b/BTCPayServer.Tests/ApiKeysTests.cs @@ -311,7 +311,7 @@ namespace BTCPayServer.Tests return (T)Convert.ChangeType(rawJson, typeof(T)); } - return JsonConvert.DeserializeObject(rawJson); + return JsonConvert.DeserializeObject(rawJson, BTCPayServer.Client.Serializer.GlobalSerializerSettings); } } } diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index b3569dcbe..b32e6e828 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Controllers; -using BTCPayServer.Controllers.RestApi.Users; using BTCPayServer.Services; using BTCPayServer.Tests.Logging; using Microsoft.AspNet.SignalR.Client; From 432d6bb2611e3e0877f23182f63221ff4d23391a Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 20 Mar 2020 14:33:11 +0900 Subject: [PATCH 304/810] Update documentation --- BTCPayServer/Controllers/ManageController.APIKeys.cs | 4 ++-- BTCPayServer/wwwroot/swagger/v1/swagger.template.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/BTCPayServer/Controllers/ManageController.APIKeys.cs b/BTCPayServer/Controllers/ManageController.APIKeys.cs index 169a8b48e..429741e2c 100644 --- a/BTCPayServer/Controllers/ManageController.APIKeys.cs +++ b/BTCPayServer/Controllers/ManageController.APIKeys.cs @@ -351,9 +351,9 @@ namespace BTCPayServer.Controllers {BTCPayServer.Client.Policies.Unrestricted, ("Unrestricted access", "The app will have unrestricted access to your account.")}, {BTCPayServer.Client.Policies.CanCreateUser, ("Create new users", "The app will be able to create new users on this server.")}, {BTCPayServer.Client.Policies.CanModifyStoreSettings, ("Modify your stores", "The app will be able to create, view and modify, delete and create new invoices on the all your stores.")}, - {BTCPayServer.Client.Policies.CanViewStoreSettings, ("View your stores", "The app will be able to create, view all your stores.")}, + {BTCPayServer.Client.Policies.CanViewStoreSettings, ("View your stores", "The app will be able to view stores settings.")}, {$"{BTCPayServer.Client.Policies.CanModifyStoreSettings}:", ("Manage selected stores", "The app will be able to view, modify, delete and create new invoices on the selected stores.")}, - {BTCPayServer.Client.Policies.CanModifyServerSettings, ("Manage your server", "The app will have total control on your server")}, + {BTCPayServer.Client.Policies.CanModifyServerSettings, ("Manage your server", "The app will have total control on the server settings of your server")}, {BTCPayServer.Client.Policies.CanViewProfile, ("View your profile", "The app will be able to view your user profile.")}, {BTCPayServer.Client.Policies.CanModifyProfile, ("Manage your profile", "The app will be able to view and modify your user profile.")}, {BTCPayServer.Client.Policies.CanCreateInvoice, ("Create an invoice", "The app will be able to create new invoice.")}, diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json index 7f06f6af1..7157fb603 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json @@ -358,7 +358,7 @@ "securitySchemes": { "APIKey": { "type": "apiKey", - "description": "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: token {token}. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n You can create API Keys in the user profile / Api Keys. (Available are: ServerManagement, StoreManagement, ProfileManagement)", + "description": "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: token {token}. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n The following permissions applies to the context of the user creating the API Key:\n * `unrestricted`: Allow unrestricted access to your account.\n * `btcpay.server.canmodifyserversettings`: Allow total control on the server settings. (only if user is administrator)\n * `btcpay.server.cancreateuser`: Allow the creation new users on this server. (only if user is administrator)\n * `btcpay.user.canviewprofile`: Allow view access to your user profile.\n * `btcpay.user.canmodifyprofile`: Allow view and modification access to your user profile.\n\nThe following permissions applies to all stores of the user, you can limit to a specific store with the following format: `btcpay.store.cancreateinvoice:6HSHAEU4iYWtjxtyRs9KyPjM9GAQp8kw2T9VWbGG1FnZ`:\n * `btcpay.store.canviewstoresettings`: Allow view access to the stores settings. \n * `btcpay.store.canmodifystoresettings`: Allow view and modification access to the stores settings.\n * `btcpay.store.cancreateinvoice`: Allow invoice creation of the store.\n\nNote that API Keys only limits permission of a user and can never expand it. If an API Key has the permission `btcpay.server.canmodifyserversettings` but that the user account creating this API Key is not administrator, the API Key will not be able to modify the server settings.\n", "name": "Authorization", "in": "header" }, From b54a7b80e3e1427e726493d28709d7b211b650ba Mon Sep 17 00:00:00 2001 From: Kukks Date: Fri, 20 Mar 2020 09:31:22 +0100 Subject: [PATCH 305/810] add tests and fix --- .../Liquid/ElementsLikeBtcPayNetwork.cs | 6 ++++- BTCPayServer.Tests/BTCPayServerTester.cs | 4 +++ BTCPayServer.Tests/ElementsTests.cs | 25 ++++++++++++++++--- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs b/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs index 45ceed889..9d059ec33 100644 --- a/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs +++ b/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs @@ -25,7 +25,11 @@ namespace BTCPayServer public override string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue) { - return $"{base.GenerateBIP21(cryptoInfoAddress, new Money(long.Parse(cryptoInfoDue.ToString(false, true).Replace(".", ""))))}&assetid={AssetId}"; + //precision 0: 10 = 0.00000010 + //precision 2: 10 = 0.00001000 + //precision 8: 10 = 10 + var money = new Money(cryptoInfoDue.ToDecimal(MoneyUnit.BTC) / decimal.Parse("1".PadRight(1 + 8 - Divisibility, '0')), MoneyUnit.BTC); + return $"{base.GenerateBIP21(cryptoInfoAddress, money)}&assetid={AssetId}"; } } } diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index 03aa4ef5d..ea72df659 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -223,6 +223,10 @@ namespace BTCPayServer.Tests var bitfinex = new MockRateProvider(); bitfinex.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("UST_BTC"), new BidAsk(0.000136m))); rateProvider.Providers.Add("bitfinex", bitfinex); + + var bitpay = new MockRateProvider(); + bitpay.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("ETB_BTC"), new BidAsk(0.1m))); + rateProvider.Providers.Add("bitpay", bitpay); } diff --git a/BTCPayServer.Tests/ElementsTests.cs b/BTCPayServer.Tests/ElementsTests.cs index 0ff0b5d63..e5cda0aca 100644 --- a/BTCPayServer.Tests/ElementsTests.cs +++ b/BTCPayServer.Tests/ElementsTests.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Memory; using NBitcoin; +using NBitcoin.Payment; using NBitcoin.RPC; using NBitpayClient; using Xunit; @@ -79,20 +80,28 @@ namespace BTCPayServer.Tests user.GrantAccess(); user.RegisterDerivationScheme("LBTC"); user.RegisterDerivationScheme("USDT"); - + user.RegisterDerivationScheme("ETB"); + await tester.LBTCExplorerNode.GenerateAsync(4); //no tether on our regtest, lets create it and set it var tether = tester.NetworkProvider.GetNetwork("USDT"); var lbtc = tester.NetworkProvider.GetNetwork("LBTC"); + var etb = tester.NetworkProvider.GetNetwork("ETB"); var issueAssetResult = await tester.LBTCExplorerNode.SendCommandAsync("issueasset", 100000, 0); tether.AssetId = uint256.Parse(issueAssetResult.Result["asset"].ToString()); ((ElementsBTCPayNetwork)tester.PayTester.GetService().GetWallet("USDT").Network) .AssetId = tether.AssetId; - Logs.Tester.LogInformation($"Asset is {tether.AssetId}"); Assert.Equal(tether.AssetId, tester.NetworkProvider.GetNetwork("USDT").AssetId); Assert.Equal(tether.AssetId, ((ElementsBTCPayNetwork)tester.PayTester.GetService().GetWallet("USDT").Network).AssetId); + + var issueAssetResult2 = await tester.LBTCExplorerNode.SendCommandAsync("issueasset", 100000, 0); + etb.AssetId = uint256.Parse(issueAssetResult2.Result["asset"].ToString()); + ((ElementsBTCPayNetwork)tester.PayTester.GetService().GetWallet("ETB").Network) + .AssetId = etb.AssetId; + + //test: register 2 assets on the same elements network and make sure paying an invoice on one does not affect the other in any way var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.1m, "BTC")); - Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count); + Assert.Equal(3, invoice.SupportedTransactionCurrencies.Count); var ci = invoice.CryptoInfo.Single(info => info.CryptoCode.Equals("LBTC")); //1 lbtc = 1 btc Assert.Equal(1, ci.Rate); @@ -109,7 +118,7 @@ namespace BTCPayServer.Tests invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.1m, "BTC")); ci = invoice.CryptoInfo.Single(info => info.CryptoCode.Equals("USDT")); - Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count); + Assert.Equal(3, invoice.SupportedTransactionCurrencies.Count); star = await tester.LBTCExplorerNode.SendCommandAsync("sendtoaddress", ci.Address, ci.Due, "", "", false, true, 1, "UNSET", tether.AssetId); @@ -120,6 +129,14 @@ namespace BTCPayServer.Tests Assert.Single(localInvoice.CryptoInfo.Single(info => info.CryptoCode.Equals("USDT", StringComparison.InvariantCultureIgnoreCase)).Payments); }); + //test precision based on https://github.com/ElementsProject/elements/issues/805#issuecomment-601277606 + var etbBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.Single(info => info.CryptoCode == "ETB").PaymentUrls.BIP21.Replace(etb.UriScheme, "bitcoin"), etb.NBitcoinNetwork); + //precision = 2, 1ETB = 0.00000100 + Assert.Equal( 100,etbBip21.Amount.Satoshi); + + var lbtcBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.Single(info => info.CryptoCode == "LBTC").PaymentUrls.BIP21.Replace(lbtc.UriScheme, "bitcoin"), lbtc.NBitcoinNetwork); + //precision = 8, 0.1 = 0.1 + Assert.Equal( 0.1m,lbtcBip21.Amount.ToDecimal(MoneyUnit.BTC)); } } } From 4666238e38c9ff1d20c9a50712455defe3719550 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 20 Mar 2020 18:38:21 +0900 Subject: [PATCH 306/810] Fix build --- BTCPayServer.Tests/GreenfieldAPITests.cs | 4 ++-- BTCPayServer/Controllers/RestApi/UsersController.cs | 3 ++- BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml | 2 +- BTCPayServer/wwwroot/swagger/v1/swagger.template.json | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index b32e6e828..4f856b63c 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -102,8 +102,8 @@ namespace BTCPayServer.Tests await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" })); await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true })); - // However, should be ok with the server management permissions - adminClient = await adminAcc.CreateClient(Policies.CanModifyServerSettings); + // However, should be ok with the unrestricted permissions of an admin + adminClient = await adminAcc.CreateClient(Policies.Unrestricted); await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" }); // Even creating new admin should be ok await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true }); diff --git a/BTCPayServer/Controllers/RestApi/UsersController.cs b/BTCPayServer/Controllers/RestApi/UsersController.cs index 9abefacee..30e57d725 100644 --- a/BTCPayServer/Controllers/RestApi/UsersController.cs +++ b/BTCPayServer/Controllers/RestApi/UsersController.cs @@ -87,7 +87,8 @@ namespace BTCPayServer.Controllers.RestApi if (anyAdmin && request.IsAdministrator is true && !isAuth) return Forbid(AuthenticationSchemes.ApiKey); // You are de-facto admin if there is no other admin, else you need to be auth and pass policy requirements - bool isAdmin = anyAdmin ? (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanModifyServerSettings))).Succeeded + bool isAdmin = anyAdmin ? (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanModifyServerSettings))).Succeeded + && (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.Unrestricted))).Succeeded && isAuth : true; // You need to be admin to create an admin diff --git a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml index 6a431637c..164c53c3a 100644 --- a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml +++ b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml @@ -7,7 +7,7 @@ Layout = "_Layout"; ViewData["Title"] = $"Authorize {(Model.ApplicationName ?? "Application")}"; var permissions = Permission.ToPermissions(Model.Permissions); - var hasStorePermission = permissions.Any(p => p.Policy == Permission.CanModifyStoreSettings); + var hasStorePermission = permissions.Any(p => p.Policy == Policies.CanModifyStoreSettings); } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json index 7157fb603..70e8aa3f1 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json @@ -143,7 +143,7 @@ }, "isAdministrator": { "type": "boolean", - "description": "Make this user administrator (only if your APIKey has ServerManagment permission)", + "description": "Make this user administrator (only if your APIKey has `unrestricted` permission of a server administrator)", "nullable": true, "default": false } From 53e7c84e73bbe3ea415472aea5c45246e47925ec Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 20 Mar 2020 18:56:30 +0900 Subject: [PATCH 307/810] Fix tests --- BTCPayServer.Client/BTCPayServerClient.cs | 4 +-- BTCPayServer.Client/Models/ApiKeyData.cs | 4 +++ BTCPayServer.Client/Serializer.cs | 33 ----------------------- BTCPayServer.Tests/ApiKeysTests.cs | 2 +- BTCPayServer.Tests/GreenfieldAPITests.cs | 2 +- 5 files changed, 8 insertions(+), 37 deletions(-) delete mode 100644 BTCPayServer.Client/Serializer.cs diff --git a/BTCPayServer.Client/BTCPayServerClient.cs b/BTCPayServer.Client/BTCPayServerClient.cs index 4dfad35db..e84031fa1 100644 --- a/BTCPayServer.Client/BTCPayServerClient.cs +++ b/BTCPayServer.Client/BTCPayServerClient.cs @@ -40,7 +40,7 @@ namespace BTCPayServer.Client protected async Task HandleResponse(HttpResponseMessage message) { HandleResponse(message); - return JsonConvert.DeserializeObject(await message.Content.ReadAsStringAsync(), Serializer.GlobalSerializerSettings); + return JsonConvert.DeserializeObject(await message.Content.ReadAsStringAsync()); } protected virtual HttpRequestMessage CreateHttpRequest(string path, @@ -68,7 +68,7 @@ namespace BTCPayServer.Client var request = CreateHttpRequest(path, queryPayload, method); if (typeof(T).IsPrimitive || !EqualityComparer.Default.Equals(bodyPayload, default(T))) { - request.Content = new StringContent(JsonConvert.SerializeObject(bodyPayload, Serializer.GlobalSerializerSettings), Encoding.UTF8, "application/json"); + request.Content = new StringContent(JsonConvert.SerializeObject(bodyPayload), Encoding.UTF8, "application/json"); } return request; diff --git a/BTCPayServer.Client/Models/ApiKeyData.cs b/BTCPayServer.Client/Models/ApiKeyData.cs index 244859f60..be635150e 100644 --- a/BTCPayServer.Client/Models/ApiKeyData.cs +++ b/BTCPayServer.Client/Models/ApiKeyData.cs @@ -1,3 +1,6 @@ +using BTCPayServer.Client.JsonConverters; +using Newtonsoft.Json; + namespace BTCPayServer.Client.Models { public class ApiKeyData @@ -5,6 +8,7 @@ namespace BTCPayServer.Client.Models public string ApiKey { get; set; } public string Label { get; set; } public string UserId { get; set; } + [JsonProperty(ItemConverterType = typeof(PermissionJsonConverter))] public Permission[] Permissions { get; set; } } } diff --git a/BTCPayServer.Client/Serializer.cs b/BTCPayServer.Client/Serializer.cs deleted file mode 100644 index 91803026e..000000000 --- a/BTCPayServer.Client/Serializer.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using BTCPayServer.Client.JsonConverters; -using Newtonsoft.Json; - -namespace BTCPayServer.Client -{ - public class Serializer - { - - private static JsonSerializerSettings _GlobalSerializerSettings; - public static JsonSerializerSettings GlobalSerializerSettings - { - get - { - if (_GlobalSerializerSettings is null) - { - var serializer = new JsonSerializerSettings(); - RegisterConverters(serializer); - _GlobalSerializerSettings = serializer; - } - return _GlobalSerializerSettings; - } - } - public static void RegisterConverters(JsonSerializerSettings settings) - { - if (settings == null) - throw new ArgumentNullException(nameof(settings)); - settings.Converters.Add(new PermissionJsonConverter()); - } - } -} diff --git a/BTCPayServer.Tests/ApiKeysTests.cs b/BTCPayServer.Tests/ApiKeysTests.cs index 5dcc68dc9..fde2e5433 100644 --- a/BTCPayServer.Tests/ApiKeysTests.cs +++ b/BTCPayServer.Tests/ApiKeysTests.cs @@ -311,7 +311,7 @@ namespace BTCPayServer.Tests return (T)Convert.ChangeType(rawJson, typeof(T)); } - return JsonConvert.DeserializeObject(rawJson, BTCPayServer.Client.Serializer.GlobalSerializerSettings); + return JsonConvert.DeserializeObject(rawJson); } } } diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 4f856b63c..6bab202d4 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -141,7 +141,7 @@ namespace BTCPayServer.Tests user.GrantAccess(); await user.MakeAdmin(); var clientProfile = await user.CreateClient(Policies.CanModifyProfile); - var clientServer = await user.CreateClient(Policies.CanModifyServerSettings, Policies.CanViewProfile); + var clientServer = await user.CreateClient(Policies.CanCreateUser, Policies.CanViewProfile); var clientInsufficient = await user.CreateClient(Policies.CanModifyStoreSettings); From 094307d688467a521b5421e624a3afb6c4175fb3 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 20 Mar 2020 19:39:02 +0900 Subject: [PATCH 308/810] Remove warning and improve UI of permission selection --- BTCPayServer/Views/Manage/AddApiKey.cshtml | 116 +++++++-------- .../Views/Manage/AuthorizeAPIKey.cshtml | 135 +++++++++--------- 2 files changed, 127 insertions(+), 124 deletions(-) diff --git a/BTCPayServer/Views/Manage/AddApiKey.cshtml b/BTCPayServer/Views/Manage/AddApiKey.cshtml index 5471ae6e8..a44b3b16b 100644 --- a/BTCPayServer/Views/Manage/AddApiKey.cshtml +++ b/BTCPayServer/Views/Manage/AddApiKey.cshtml @@ -8,8 +8,8 @@ }

    @ViewData["Title"]

    - -

    + +

    Generate a new api key to use BTCPay through its API.

    @@ -29,80 +29,81 @@ @for (int i = 0; i < Model.PermissionValues.Count; i++) { - @if (!Model.PermissionValues[i].Forbidden) + @if (!Model.PermissionValues[i].Forbidden) { -
    +
    +
    - - + + -

    @Model.PermissionValues[i].Description

    + @Model.PermissionValues[i].Description
    +
    } } @if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores) { -
    - - - - -

    @Model.StoreManagementPermission.Description

    - -
    +
    +
    + + + + + @Model.StoreManagementPermission.Description +
    +
    } else if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.Specific) { -
    -
  • -
    @Model.StoreManagementSelectivePermission.Title
    -

    @Model.StoreManagementSelectivePermission.Description

    - -
  • - @if (!Model.Stores.Any()) +
  • +
    @Model.StoreManagementSelectivePermission.Title
    + @Model.StoreManagementSelectivePermission.Description + +
  • + @if (!Model.Stores.Any()) { -
  • - You currently have no stores configured. -
  • +
  • + You currently have no stores configured. +
  • } - @for (var index = 0; index < Model.SpecificStores.Count; index++) + @for (var index = 0; index < Model.SpecificStores.Count; index++) { -
    -
    -
    -
    - @if (Model.SpecificStores[index] == null) +
    +
    +
    +
    + @if (Model.SpecificStores[index] == null) { - + } else { var store = Model.Stores.SingleOrDefault(data => data.Id == Model.SpecificStores[index]); - + } - -
    -
    -
    - - -
    -
    +
    - } - @if (Model.SpecificStores.Count < Model.Stores.Length) - { -
    - -
    - } +
    +
    + + +
    +
    + } + @if (Model.SpecificStores.Count < Model.Stores.Length) + { +
    + +
    + } } @@ -113,12 +114,13 @@ @await Html.PartialAsync("_ValidationScriptsPartial") } diff --git a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml index 164c53c3a..2c48d067e 100644 --- a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml +++ b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml @@ -10,7 +10,7 @@ var hasStorePermission = permissions.Any(p => p.Policy == Policies.CanModifyStoreSettings); } - +
    @@ -38,112 +38,113 @@
    @if (!permissions.Any()) { -
    -

    There are no associated permissions to the API key being requested here. The application cannot do anything with your BTCPay account other than validating your account exists.

    -
    +
    +

    There are no associated permissions to the API key being requested here. The application cannot do anything with your BTCPay account other than validating your account exists.

    +
    } @for (int i = 0; i < Model.PermissionValues.Count; i++) { -
    +
    +
    @if (Model.Strict) { - - + + } else { - + } - - + @if (Model.PermissionValues[i].Forbidden) { -
    - - This permission is not available for your account. - +
    + + This permission is not available for your account. + } -

    @Model.PermissionValues[i].Description

    + @Model.PermissionValues[i].Description
    +
    } @if (hasStorePermission) { - @if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores) + @if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores) { -
    - @if (Model.Strict) +
    +
    + @if (Model.Strict) { - - + + } else { - + } - -
    - -

    @Model.StoreManagementPermission.Description

    - @if (Model.SelectiveStores) + + @if (Model.SelectiveStores) { - + } -
    +
    + + @Model.StoreManagementPermission.Description +
    +
    } else if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.Specific) { -
    -
  • -
    @Model.StoreManagementSelectivePermission.Title
    -

    @Model.StoreManagementSelectivePermission.Description

    - -
  • - @if (!Model.Stores.Any()) +
    +
    @Model.StoreManagementSelectivePermission.Title
    + @Model.StoreManagementSelectivePermission.Description + +
    + @if (!Model.Stores.Any()) { -
  • - You currently have no stores configured. -
  • +
    + You currently have no stores configured. +
    } - @for (var index = 0; index < Model.SpecificStores.Count; index++) + @for (var index = 0; index < Model.SpecificStores.Count; index++) { -
    -
    -
    -
    - @if (Model.SpecificStores[index] == null) +
    +
    +
    +
    + @if (Model.SpecificStores[index] == null) { - + } else { var store = Model.Stores.SingleOrDefault(data => data.Id == Model.SpecificStores[index]); - + } - -
    -
    -
    - - -
    -
    -
    - } - @if (Model.SpecificStores.Count < Model.Stores.Length) - { -
    - -
    - } + +
    +
    + + +
    +
    +
    + } + @if (Model.SpecificStores.Count < Model.Stores.Length) + { +
    + +
    + } } }
    From ad4dbdad6d4becdc03b85e4042deb61f85ded178 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 20 Mar 2020 19:57:00 +0900 Subject: [PATCH 309/810] Fix the PermissionJsonConverter --- .../JsonConverters/PermissionJsonConverter.cs | 2 +- .../Security/APIKeys/APIKeyAuthorizationHandler.cs | 1 + BTCPayServer/Views/Manage/AddApiKey.cshtml | 8 ++++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/BTCPayServer.Client/JsonConverters/PermissionJsonConverter.cs b/BTCPayServer.Client/JsonConverters/PermissionJsonConverter.cs index 26bf1eace..663fff089 100644 --- a/BTCPayServer.Client/JsonConverters/PermissionJsonConverter.cs +++ b/BTCPayServer.Client/JsonConverters/PermissionJsonConverter.cs @@ -20,7 +20,7 @@ namespace BTCPayServer.Client.JsonConverters return null; if (reader.TokenType != JsonToken.String) throw new JsonObjectException("Type 'Permission' is expected to be a 'String'", reader); - if (reader.ReadAsString() is String s && Permission.TryParse(s, out var permission)) + if (reader.Value is String s && Permission.TryParse(s, out var permission)) return permission; throw new JsonObjectException("Invalid 'Permission' String", reader); } diff --git a/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs b/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs index a4306ff37..2b5b147e0 100644 --- a/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs +++ b/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs @@ -38,6 +38,7 @@ namespace BTCPayServer.Security.APIKeys { case Policies.CanModifyProfile: case Policies.CanViewProfile: + case Policies.Unrestricted: success = context.HasPermission(Permission.Create(requirement.Policy)); break; diff --git a/BTCPayServer/Views/Manage/AddApiKey.cshtml b/BTCPayServer/Views/Manage/AddApiKey.cshtml index a44b3b16b..8c1866aed 100644 --- a/BTCPayServer/Views/Manage/AddApiKey.cshtml +++ b/BTCPayServer/Views/Manage/AddApiKey.cshtml @@ -56,16 +56,16 @@ } else if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.Specific) { -
  • +
    @Model.StoreManagementSelectivePermission.Title
    @Model.StoreManagementSelectivePermission.Description -
  • +
    @if (!Model.Stores.Any()) { -
  • +
    You currently have no stores configured. -
  • +
    } @for (var index = 0; index < Model.SpecificStores.Count; index++) { From d2864ccd7c3d06f259f517d4522d4b6b41e6b659 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 20 Mar 2020 20:00:05 +0900 Subject: [PATCH 310/810] Make sure ApiKeyData set all the fields, remove UserId --- BTCPayServer.Client/Models/ApiKeyData.cs | 1 - BTCPayServer/Controllers/RestApi/ApiKeysController.cs | 3 +-- BTCPayServer/wwwroot/swagger/v1/swagger.template.json | 8 ++------ 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/BTCPayServer.Client/Models/ApiKeyData.cs b/BTCPayServer.Client/Models/ApiKeyData.cs index be635150e..d9145b68a 100644 --- a/BTCPayServer.Client/Models/ApiKeyData.cs +++ b/BTCPayServer.Client/Models/ApiKeyData.cs @@ -7,7 +7,6 @@ namespace BTCPayServer.Client.Models { public string ApiKey { get; set; } public string Label { get; set; } - public string UserId { get; set; } [JsonProperty(ItemConverterType = typeof(PermissionJsonConverter))] public Permission[] Permissions { get; set; } } diff --git a/BTCPayServer/Controllers/RestApi/ApiKeysController.cs b/BTCPayServer/Controllers/RestApi/ApiKeysController.cs index d512552d7..883a32018 100644 --- a/BTCPayServer/Controllers/RestApi/ApiKeysController.cs +++ b/BTCPayServer/Controllers/RestApi/ApiKeysController.cs @@ -47,8 +47,7 @@ namespace BTCPayServer.Controllers.RestApi { Permissions = Permission.ToPermissions(data.Permissions).ToArray(), ApiKey = data.Id, - UserId = data.UserId, - Label = data.Label + Label = data.Label ?? string.Empty }; } } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json index 70e8aa3f1..fb969b8c2 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json @@ -335,15 +335,11 @@ "properties": { "apiKey": { "type": "string", - "nullable": true + "nullable": false }, "label": { "type": "string", - "nullable": true - }, - "userId": { - "type": "string", - "nullable": true + "nullable": false }, "permissions": { "type": "array", From 9a940a044ee68dd64cf92ed71ee53e9f54b04b6c Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 20 Mar 2020 20:00:30 +0900 Subject: [PATCH 311/810] Fix tests --- BTCPayServer.Tests/GreenfieldAPITests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 6bab202d4..f3977cabc 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -42,7 +42,6 @@ namespace BTCPayServer.Tests var apiKeyData = await client.GetCurrentAPIKeyInfo(); Assert.NotNull(apiKeyData); Assert.Equal(client.APIKey, apiKeyData.ApiKey); - Assert.Equal(user.UserId, apiKeyData.UserId); Assert.Equal(2, apiKeyData.Permissions.Length); //revoke current api key From 7e7f0053e21a006e4707684a29abcf7358d3eed0 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 20 Mar 2020 20:25:10 +0900 Subject: [PATCH 312/810] Improve documentation --- BTCPayServer.Tests/GreenfieldAPITests.cs | 9 +-- .../Controllers/RestApi/ApiKeysController.cs | 1 + .../wwwroot/swagger/v1/swagger.template.json | 60 +++++++------------ 3 files changed, 27 insertions(+), 43 deletions(-) diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index f3977cabc..bea94c54f 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -37,19 +37,16 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); user.GrantAccess(); await user.MakeAdmin(); - var client = await user.CreateClient(Policies.CanModifyServerSettings, Policies.CanModifyStoreSettings); + var client = await user.CreateClient(Policies.Unrestricted); //Get current api key var apiKeyData = await client.GetCurrentAPIKeyInfo(); Assert.NotNull(apiKeyData); Assert.Equal(client.APIKey, apiKeyData.ApiKey); - Assert.Equal(2, apiKeyData.Permissions.Length); + Assert.Single(apiKeyData.Permissions); //revoke current api key await client.RevokeCurrentAPIKeyInfo(); - await Assert.ThrowsAsync(async () => - { - await client.GetCurrentAPIKeyInfo(); - }); + await AssertHttpError(401, async () => await client.GetCurrentAPIKeyInfo()); } } diff --git a/BTCPayServer/Controllers/RestApi/ApiKeysController.cs b/BTCPayServer/Controllers/RestApi/ApiKeysController.cs index 883a32018..6a3299418 100644 --- a/BTCPayServer/Controllers/RestApi/ApiKeysController.cs +++ b/BTCPayServer/Controllers/RestApi/ApiKeysController.cs @@ -34,6 +34,7 @@ namespace BTCPayServer.Controllers.RestApi } [HttpDelete("~/api/v1/api-keys/current")] + [Authorize(Policy = Policies.Unrestricted, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] public async Task> RevokeKey() { ControllerContext.HttpContext.GetAPIKey(out var apiKey); diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json index fb969b8c2..8fe9afc8d 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json @@ -20,11 +20,10 @@ ], "summary": "Authorize User", "description": "Redirect the browser to this endpoint to request the user to generate an api-key with specific permissions", - "operationId": "Manage_AuthorizeAPIKey", "parameters": [ { "name": "permissions", - "description": "The permissions to request. (See APIKey authentication)", + "description": "The permissions to request. (See GreenField Authentication)", "in": "query", "style": "form", "explode": true, @@ -53,7 +52,8 @@ "in": "query", "schema": { "type": "boolean", - "default": true + "default": true, + "nullable": true }, "x-position": 3 }, @@ -63,29 +63,22 @@ "in": "query", "schema": { "type": "boolean", - "default": false + "default": false, + "nullable": true }, "x-position": 4 } ], "responses": { "200": { - "description": "", + "description": "A HTML form that a user can use to confirm permissions to grant", "content": { - "application/octet-stream": { - "schema": { - "type": "string", - "format": "binary" - } + "text/html": { } } } }, - "security": [ - { - "APIKey": [] - } - ] + "security": [ ] } }, "/api/v1/users/me": { @@ -110,8 +103,8 @@ }, "security": [ { - "APIKey": [ - "ProfileManagement" + "GreenField Authentication": [ + "btcpay.user.canviewprofile" ] } ] @@ -123,7 +116,7 @@ "Users" ], "summary": "Create user", - "description": "Create a new user. This operation can be called without authentication if there is not any administrator yet on the server.", + "description": "Create a new user.\n\nThis operation can be called without authentication in any of this cases:\n* There is not any administrator yet on the server,\n* The subscriptions are not disabled in the server's policies.\n\nIf the first administrator is created by this call, subscriptions are automatically disabled.", "requestBody": { "x-name": "request", "content": { @@ -143,7 +136,7 @@ }, "isAdministrator": { "type": "boolean", - "description": "Make this user administrator (only if your APIKey has `unrestricted` permission of a server administrator)", + "description": "Make this user administrator (only if you have the `unrestricted` permission of a server administrator)", "nullable": true, "default": false } @@ -187,10 +180,9 @@ }, "security": [ { - "APIKey": [ - "ServerManagement" - ], - "Anonymous": [ ] + "GreenField Authentication": [ + "btcpay.server.cancreateuser" + ] } ] } @@ -202,7 +194,6 @@ ], "summary": "Get current API Key information", "description": "View information about the current API key", - "operationId": "ApiKeys_GetKey", "responses": { "200": { "description": "Information about the current api key", @@ -217,7 +208,7 @@ }, "security": [ { - "APIKey": [] + "GreenField Authentication": [] } ] }, @@ -227,7 +218,6 @@ ], "summary": "Revoke the current API Key", "description": "Revoke the current API key so that it cannot be used anymore", - "operationId": "ApiKeys_RevokeKey", "responses": { "200": { "description": "The key was revoked and is no longer usable", @@ -242,7 +232,7 @@ }, "security": [ { - "APIKey": [] + "GreenField Authentication": [ "unrestricted" ] } ] } @@ -335,15 +325,18 @@ "properties": { "apiKey": { "type": "string", + "description": "The API Key to use for Greenfield Authentication", "nullable": false }, "label": { "type": "string", + "description": "The label given by the user to this API Key", "nullable": false }, "permissions": { "type": "array", - "nullable": true, + "description": "The permissions associated to this API Key", + "nullable": false, "items": { "type": "string" } @@ -352,24 +345,17 @@ } }, "securitySchemes": { - "APIKey": { + "GreenField Authentication": { "type": "apiKey", "description": "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: token {token}. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n The following permissions applies to the context of the user creating the API Key:\n * `unrestricted`: Allow unrestricted access to your account.\n * `btcpay.server.canmodifyserversettings`: Allow total control on the server settings. (only if user is administrator)\n * `btcpay.server.cancreateuser`: Allow the creation new users on this server. (only if user is administrator)\n * `btcpay.user.canviewprofile`: Allow view access to your user profile.\n * `btcpay.user.canmodifyprofile`: Allow view and modification access to your user profile.\n\nThe following permissions applies to all stores of the user, you can limit to a specific store with the following format: `btcpay.store.cancreateinvoice:6HSHAEU4iYWtjxtyRs9KyPjM9GAQp8kw2T9VWbGG1FnZ`:\n * `btcpay.store.canviewstoresettings`: Allow view access to the stores settings. \n * `btcpay.store.canmodifystoresettings`: Allow view and modification access to the stores settings.\n * `btcpay.store.cancreateinvoice`: Allow invoice creation of the store.\n\nNote that API Keys only limits permission of a user and can never expand it. If an API Key has the permission `btcpay.server.canmodifyserversettings` but that the user account creating this API Key is not administrator, the API Key will not be able to modify the server settings.\n", "name": "Authorization", "in": "header" - }, - "Anonymous": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "Nothing required", - "description": "Anonymous authentication" } } }, "security": [ { - "APIKey": [], - "Anonymous": [] + "GreenField Authentication": [] } ], "tags": [ From 478b1463ffd68227373d94c895108c7080f0005a Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 20 Mar 2020 20:26:36 +0900 Subject: [PATCH 313/810] Improve documentation --- BTCPayServer/wwwroot/swagger/v1/swagger.template.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json index 8fe9afc8d..52a48c965 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json @@ -172,7 +172,7 @@ "description": "If you need to authenticate for this endpoint (ie. the server settings policies lock subscriptions and that an admin already exists)" }, "403": { - "description": "If you are authenticated but forbidden to create a new user (ie. you don't have the ServerManagement permission)" + "description": "If you are authenticated but forbidden to create a new user (ie. you don't have the `unrestricted` permission on a server administrator or if you are not administrator and registrations are disabled in the server's policies)" }, "429": { "description": "DDoS protection if you are creating more than 2 accounts every minutes (non-admin only)" From 9d99c32305ece006d22c2b2857a224d2c9bff0cd Mon Sep 17 00:00:00 2001 From: Kukks Date: Fri, 20 Mar 2020 14:05:23 +0100 Subject: [PATCH 314/810] add basic auth for greenfield --- .../Controllers/RestApi/ApiKeysController.cs | 5 +- .../RestApi/TestApiKeyController.cs | 15 ++--- .../Controllers/RestApi/UsersController.cs | 17 ++--- BTCPayServer/Hosting/BTCPayServerServices.cs | 3 +- .../Security/APIKeys/APIKeyExtensions.cs | 5 -- .../Security/AuthenticationSchemes.cs | 2 + .../Basic/BasicAuthenticationHandler.cs | 65 +++++++++++++++++++ .../Basic/BasicAuthenticationOptions.cs | 8 +++ .../Security/Basic/BasicExtensions.cs | 17 +++++ .../wwwroot/swagger/v1/swagger.template.json | 27 +++++--- 10 files changed, 129 insertions(+), 35 deletions(-) create mode 100644 BTCPayServer/Security/Basic/BasicAuthenticationHandler.cs create mode 100644 BTCPayServer/Security/Basic/BasicAuthenticationOptions.cs create mode 100644 BTCPayServer/Security/Basic/BasicExtensions.cs diff --git a/BTCPayServer/Controllers/RestApi/ApiKeysController.cs b/BTCPayServer/Controllers/RestApi/ApiKeysController.cs index 6a3299418..fff308122 100644 --- a/BTCPayServer/Controllers/RestApi/ApiKeysController.cs +++ b/BTCPayServer/Controllers/RestApi/ApiKeysController.cs @@ -6,14 +6,13 @@ using BTCPayServer.Data; using BTCPayServer.Security; using BTCPayServer.Security.APIKeys; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; namespace BTCPayServer.Controllers.RestApi { [ApiController] - [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] public class ApiKeysController : ControllerBase { private readonly APIKeyRepository _apiKeyRepository; @@ -34,7 +33,7 @@ namespace BTCPayServer.Controllers.RestApi } [HttpDelete("~/api/v1/api-keys/current")] - [Authorize(Policy = Policies.Unrestricted, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(Policy = Policies.Unrestricted, AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] public async Task> RevokeKey() { ControllerContext.HttpContext.GetAPIKey(out var apiKey); diff --git a/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs b/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs index 971fd63b1..3673b4fb9 100644 --- a/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs +++ b/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Security; -using BTCPayServer.Security.APIKeys; using BTCPayServer.Services.Stores; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; @@ -15,7 +14,7 @@ namespace BTCPayServer.Controllers.RestApi /// [Route("api/test/apikey")] [ApiController] - [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] public class TestApiKeyController : ControllerBase { private readonly UserManager _userManager; @@ -28,28 +27,28 @@ namespace BTCPayServer.Controllers.RestApi } [HttpGet("me/id")] - [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] public string GetCurrentUserId() { return _userManager.GetUserId(User); } [HttpGet("me")] - [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] public async Task GetCurrentUser() { return await _userManager.GetUserAsync(User); } [HttpGet("me/is-admin")] - [Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] public bool AmIAnAdmin() { return true; } [HttpGet("me/stores")] - [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] public StoreData[] GetCurrentUserStores() { return this.HttpContext.GetStoresData(); @@ -57,7 +56,7 @@ namespace BTCPayServer.Controllers.RestApi [HttpGet("me/stores/{storeId}/can-view")] [Authorize(Policy = Policies.CanViewStoreSettings, - AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] public bool CanViewStore(string storeId) { return true; @@ -65,7 +64,7 @@ namespace BTCPayServer.Controllers.RestApi [HttpGet("me/stores/{storeId}/can-edit")] [Authorize(Policy = Policies.CanModifyStoreSettings, - AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] public bool CanEditStore(string storeId) { return true; diff --git a/BTCPayServer/Controllers/RestApi/UsersController.cs b/BTCPayServer/Controllers/RestApi/UsersController.cs index 30e57d725..301d9df46 100644 --- a/BTCPayServer/Controllers/RestApi/UsersController.cs +++ b/BTCPayServer/Controllers/RestApi/UsersController.cs @@ -1,6 +1,4 @@ -using System; using Microsoft.Extensions.Logging; -using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -13,7 +11,6 @@ using BTCPayServer.Security; using BTCPayServer.Security.APIKeys; using BTCPayServer.Services; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -23,7 +20,7 @@ using BTCPayServer.Client; namespace BTCPayServer.Controllers.RestApi { [ApiController] - [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] public class UsersController : ControllerBase { private readonly UserManager _userManager; @@ -40,8 +37,8 @@ namespace BTCPayServer.Controllers.RestApi RoleManager roleManager, SettingsRepository settingsRepository, EventAggregator eventAggregator, IPasswordValidator passwordValidator, - NicolasDorier.RateLimits.RateLimitService throttleService, - Configuration.BTCPayServerOptions options, + RateLimitService throttleService, + BTCPayServerOptions options, IAuthorizationService authorizationService) { _userManager = userManager; @@ -55,7 +52,7 @@ namespace BTCPayServer.Controllers.RestApi _authorizationService = authorizationService; } - [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] [HttpGet("~/api/v1/users/me")] public async Task> GetCurrentUser() { @@ -85,7 +82,7 @@ namespace BTCPayServer.Controllers.RestApi // Even if subscription are unlocked, it is forbidden to create admin unauthenticated if (anyAdmin && request.IsAdministrator is true && !isAuth) - return Forbid(AuthenticationSchemes.ApiKey); + return Forbid(AuthenticationSchemes.ApiKeyOrBasic); // You are de-facto admin if there is no other admin, else you need to be auth and pass policy requirements bool isAdmin = anyAdmin ? (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanModifyServerSettings))).Succeeded && (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.Unrestricted))).Succeeded @@ -93,14 +90,14 @@ namespace BTCPayServer.Controllers.RestApi : true; // You need to be admin to create an admin if (request.IsAdministrator is true && !isAdmin) - return Forbid(AuthenticationSchemes.ApiKey); + return Forbid(AuthenticationSchemes.ApiKeyOrBasic); if (!isAdmin && policies.LockSubscription) { // If we are not admin and subscriptions are locked, we need to check the Policies.CanCreateUser.Key permission var canCreateUser = (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanCreateUser))).Succeeded; if (!isAuth || !canCreateUser) - return Forbid(AuthenticationSchemes.ApiKey); + return Forbid(AuthenticationSchemes.ApiKeyOrBasic); } var user = new ApplicationUser diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 07d1282e7..c32ea9907 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -286,7 +286,8 @@ namespace BTCPayServer.Hosting services.AddAuthentication() .AddCookie() .AddBitpayAuthentication() - .AddAPIKeyAuthentication(); + .AddAPIKeyAuthentication() + .AddBasicAuthentication(); } public static IApplicationBuilder UsePayServer(this IApplicationBuilder app) diff --git a/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs b/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs index 5cf617329..d1b6a391a 100644 --- a/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs +++ b/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs @@ -1,15 +1,10 @@ using System; using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; using BTCPayServer.Client; -using BTCPayServer.Data; using BTCPayServer.Security.Bitpay; -using BTCPayServer.Services.Stores; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; diff --git a/BTCPayServer/Security/AuthenticationSchemes.cs b/BTCPayServer/Security/AuthenticationSchemes.cs index 93bcfedc5..e5d8683f9 100644 --- a/BTCPayServer/Security/AuthenticationSchemes.cs +++ b/BTCPayServer/Security/AuthenticationSchemes.cs @@ -5,5 +5,7 @@ public const string Cookie = "Identity.Application"; public const string Bitpay = "Bitpay"; public const string ApiKey = "GreenfieldApiKey"; + public const string Basic = "Basic"; + public const string ApiKeyOrBasic = "Basic,GreenfieldApiKey"; } } diff --git a/BTCPayServer/Security/Basic/BasicAuthenticationHandler.cs b/BTCPayServer/Security/Basic/BasicAuthenticationHandler.cs new file mode 100644 index 000000000..34be18070 --- /dev/null +++ b/BTCPayServer/Security/Basic/BasicAuthenticationHandler.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using BTCPayServer.Client; +using BTCPayServer.Data; +using BTCPayServer.Security.APIKeys; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace BTCPayServer.Security.Basic +{ + public class BasicAuthenticationHandler : AuthenticationHandler + { + private readonly IOptionsMonitor _identityOptions; + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + + public BasicAuthenticationHandler( + + IOptionsMonitor identityOptions, + SignInManager signInManager, + UserManager userManager, + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock) : base(options, logger, encoder, clock) + { + _identityOptions = identityOptions; + _signInManager = signInManager; + _userManager = userManager; + } + + protected override async Task HandleAuthenticateAsync() + { + + string authHeader = Context.Request.Headers["Authorization"]; + + if (authHeader == null || !authHeader.StartsWith("Basic ")) return AuthenticateResult.NoResult(); + var encodedUsernamePassword = authHeader.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries)[1]?.Trim(); + var decodedUsernamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(encodedUsernamePassword)).Split(':'); + var username = decodedUsernamePassword[0]; + var password = decodedUsernamePassword[1]; + + var result = await _signInManager.PasswordSignInAsync(username, password, true, true); + if (!result.Succeeded) return AuthenticateResult.Fail(result.ToString()); + + var user = await _userManager.FindByNameAsync(username); + var claims = new List() + { + new Claim(_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, user.Id), + new Claim(APIKeyConstants.ClaimTypes.Permission, Permission.Create(Policies.Unrestricted).ToString()) + }; + + return AuthenticateResult.Success(new AuthenticationTicket( + new ClaimsPrincipal(new ClaimsIdentity(claims, APIKeyConstants.AuthenticationType)), APIKeyConstants.AuthenticationType)); + + } + } +} diff --git a/BTCPayServer/Security/Basic/BasicAuthenticationOptions.cs b/BTCPayServer/Security/Basic/BasicAuthenticationOptions.cs new file mode 100644 index 000000000..f326d66e8 --- /dev/null +++ b/BTCPayServer/Security/Basic/BasicAuthenticationOptions.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Authentication; + +namespace BTCPayServer.Security.Basic +{ + public class BasicAuthenticationOptions : AuthenticationSchemeOptions + { + } +} diff --git a/BTCPayServer/Security/Basic/BasicExtensions.cs b/BTCPayServer/Security/Basic/BasicExtensions.cs new file mode 100644 index 000000000..fd51395b8 --- /dev/null +++ b/BTCPayServer/Security/Basic/BasicExtensions.cs @@ -0,0 +1,17 @@ +using BTCPayServer.Security.Basic; +using Microsoft.AspNetCore.Authentication; + +namespace BTCPayServer.Security.APIKeys +{ + public static class BasicExtensions + { + + public static AuthenticationBuilder AddBasicAuthentication(this AuthenticationBuilder builder) + { + builder.AddScheme(AuthenticationSchemes.Basic, + o => { }); + return builder; + } + + } +} diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json index 52a48c965..8d7dfb820 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json @@ -5,8 +5,7 @@ "description": "A full API to use your BTCPay Server", "contact": { "name": "BTCPay Server", - "url": "https://btcpayserver.org", - "email": "nicolas.dorier@gmail.com" + "url": "https://btcpayserver.org" }, "version": "v1" }, @@ -105,7 +104,8 @@ { "GreenField Authentication": [ "btcpay.user.canviewprofile" - ] + ], + "Basic": [] } ] } @@ -182,7 +182,8 @@ { "GreenField Authentication": [ "btcpay.server.cancreateuser" - ] + ], + "Basic": [] } ] } @@ -208,7 +209,8 @@ }, "security": [ { - "GreenField Authentication": [] + "GreenField Authentication": [], + "Basic": [] } ] }, @@ -232,7 +234,8 @@ }, "security": [ { - "GreenField Authentication": [ "unrestricted" ] + "GreenField Authentication": [ "unrestricted" ], + "Basic": [] } ] } @@ -347,9 +350,17 @@ "securitySchemes": { "GreenField Authentication": { "type": "apiKey", - "description": "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: token {token}. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n The following permissions applies to the context of the user creating the API Key:\n * `unrestricted`: Allow unrestricted access to your account.\n * `btcpay.server.canmodifyserversettings`: Allow total control on the server settings. (only if user is administrator)\n * `btcpay.server.cancreateuser`: Allow the creation new users on this server. (only if user is administrator)\n * `btcpay.user.canviewprofile`: Allow view access to your user profile.\n * `btcpay.user.canmodifyprofile`: Allow view and modification access to your user profile.\n\nThe following permissions applies to all stores of the user, you can limit to a specific store with the following format: `btcpay.store.cancreateinvoice:6HSHAEU4iYWtjxtyRs9KyPjM9GAQp8kw2T9VWbGG1FnZ`:\n * `btcpay.store.canviewstoresettings`: Allow view access to the stores settings. \n * `btcpay.store.canmodifystoresettings`: Allow view and modification access to the stores settings.\n * `btcpay.store.cancreateinvoice`: Allow invoice creation of the store.\n\nNote that API Keys only limits permission of a user and can never expand it. If an API Key has the permission `btcpay.server.canmodifyserversettings` but that the user account creating this API Key is not administrator, the API Key will not be able to modify the server settings.\n", + "description": "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: `token {token}`. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n The following permissions applies to the context of the user creating the API Key:\n * `unrestricted`: Allow unrestricted access to your account.\n * `btcpay.server.canmodifyserversettings`: Allow total control on the server settings. (only if user is administrator)\n * `btcpay.server.cancreateuser`: Allow the creation of new users on this server. (only if user is an administrator)\n * `btcpay.user.canviewprofile`: Allow view access to your user profile.\n * `btcpay.user.canmodifyprofile`: Allow view and modification access to your user profile.\n\nThe following permissions applies to all stores of the user, you can limit to a specific store with the following format: `btcpay.store.cancreateinvoice:6HSHAEU4iYWtjxtyRs9KyPjM9GAQp8kw2T9VWbGG1FnZ`:\n * `btcpay.store.canviewstoresettings`: Allow view access to the stores settings. \n * `btcpay.store.canmodifystoresettings`: Allow view and modification access to the stores settings.\n * `btcpay.store.cancreateinvoice`: Allow invoice creation of the store.\n\nNote that API Keys only limits permission of a user and can never expand it. If an API Key has the permission `btcpay.server.canmodifyserversettings` but that the user account creating this API Key is not administrator, the API Key will not be able to modify the server settings.\n", "name": "Authorization", - "in": "header" + "in": "header", + "scheme": "token" + }, + "Basic": { + "type": "http", + "description": "BTCPay Server supports authenticating and authorizing users through the Basic HTTP authentication scheme. Send the user and password encoded in base64 with the format `Basic {base64(username:password)}`. Using this authentication method implicitly provides you with the `unrestricted` permission", + "name": "Authorization", + "in": "header", + "scheme": "Basic" } } }, From f8f358ebdbb987d901659d9910db31acec6a0a3d Mon Sep 17 00:00:00 2001 From: Kukks Date: Fri, 20 Mar 2020 17:14:47 +0100 Subject: [PATCH 315/810] add to client, fix tests and doc --- BTCPayServer.Client/BTCPayServerClient.cs | 15 +++++++++++++ BTCPayServer.Tests/GreenfieldAPITests.cs | 21 ++++++++++++++++++- BTCPayServer.Tests/TestAccount.cs | 5 +++++ .../Controllers/RestApi/ApiKeysController.cs | 4 ++-- .../Controllers/RestApi/UsersController.cs | 6 +++--- .../Security/AuthenticationSchemes.cs | 2 +- .../wwwroot/swagger/v1/swagger.template.json | 6 ++---- 7 files changed, 48 insertions(+), 11 deletions(-) diff --git a/BTCPayServer.Client/BTCPayServerClient.cs b/BTCPayServer.Client/BTCPayServerClient.cs index e84031fa1..a5fa230f4 100644 --- a/BTCPayServer.Client/BTCPayServerClient.cs +++ b/BTCPayServer.Client/BTCPayServerClient.cs @@ -14,6 +14,8 @@ namespace BTCPayServer.Client { private readonly string _apiKey; private readonly Uri _btcpayHost; + private readonly string _username; + private readonly string _password; private readonly HttpClient _httpClient; public string APIKey => _apiKey; @@ -31,6 +33,15 @@ namespace BTCPayServer.Client _btcpayHost = btcpayHost; _httpClient = httpClient ?? new HttpClient(); } + + public BTCPayServerClient(Uri btcpayHost, string username, string password, HttpClient httpClient = null) + { + _apiKey = APIKey; + _btcpayHost = btcpayHost; + _username = username; + _password = password; + _httpClient = httpClient ?? new HttpClient(); + } protected void HandleResponse(HttpResponseMessage message) { @@ -56,6 +67,10 @@ namespace BTCPayServer.Client var httpRequest = new HttpRequestMessage(method ?? HttpMethod.Get, uriBuilder.Uri); if (_apiKey != null) httpRequest.Headers.Authorization = new AuthenticationHeaderValue("token", _apiKey); + else if (!string.IsNullOrEmpty(_username)) + { + httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic", System.Convert.ToBase64String(Encoding.ASCII.GetBytes(_username + ":" + _password))); + } return httpRequest; diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index bea94c54f..5c1129da5 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -38,15 +38,23 @@ namespace BTCPayServer.Tests user.GrantAccess(); await user.MakeAdmin(); var client = await user.CreateClient(Policies.Unrestricted); + var clientBasic = await user.CreateClient(); //Get current api key var apiKeyData = await client.GetCurrentAPIKeyInfo(); Assert.NotNull(apiKeyData); Assert.Equal(client.APIKey, apiKeyData.ApiKey); Assert.Single(apiKeyData.Permissions); - + + //a client using Basic Auth has no business here + await AssertHttpError(401, async () => await clientBasic.GetCurrentAPIKeyInfo()); + //revoke current api key await client.RevokeCurrentAPIKeyInfo(); await AssertHttpError(401, async () => await client.GetCurrentAPIKeyInfo()); + //a client using Basic Auth has no business here + await AssertHttpError(401, async () => await clientBasic.RevokeCurrentAPIKeyInfo()); + + } } @@ -108,12 +116,14 @@ namespace BTCPayServer.Tests user1Acc.UserId = user1.Id; user1Acc.IsAdmin = false; var user1Client = await user1Acc.CreateClient(Policies.CanModifyServerSettings); + var user1ClientBasic = await user1Acc.CreateClient(); // User1 trying to get server management would still fail to create user await AssertHttpError(403, async () => await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" })); // User1 should be able to create user if subscription unlocked await settings.UpdateSetting(new PoliciesSettings() { LockSubscription = false }); await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" }); + await user1ClientBasic.CreateUser(new CreateApplicationUserRequest() { Email = "testaa8@gmail.com", Password = "afewfoiewiou" }); // But not an admin await AssertHttpError(403, async () => await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "admin8@gmail.com", Password = "afewfoiewiou", IsAdministrator = true })); } @@ -139,6 +149,7 @@ namespace BTCPayServer.Tests var clientProfile = await user.CreateClient(Policies.CanModifyProfile); var clientServer = await user.CreateClient(Policies.CanCreateUser, Policies.CanViewProfile); var clientInsufficient = await user.CreateClient(Policies.CanModifyStoreSettings); + var clientBasic = await user.CreateClient(); var apiKeyProfileUserData = await clientProfile.GetCurrentUser(); @@ -149,6 +160,7 @@ namespace BTCPayServer.Tests await Assert.ThrowsAsync(async () => await clientInsufficient.GetCurrentUser()); await clientServer.GetCurrentUser(); await clientProfile.GetCurrentUser(); + await clientBasic.GetCurrentUser(); await Assert.ThrowsAsync(async () => await clientInsufficient.CreateUser(new CreateApplicationUserRequest() { @@ -163,6 +175,13 @@ namespace BTCPayServer.Tests }); Assert.NotNull(newUser); + var newUser2 = await clientBasic.CreateUser(new CreateApplicationUserRequest() + { + Email = $"{Guid.NewGuid()}@g.com", + Password = Guid.NewGuid().ToString() + }); + Assert.NotNull(newUser2); + await Assert.ThrowsAsync(async () => await clientServer.CreateUser(new CreateApplicationUserRequest() { Email = $"{Guid.NewGuid()}", diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index 9ecfe14e4..c0ef1a4b9 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -49,6 +49,11 @@ namespace BTCPayServer.Tests IsAdmin = true; } + public async Task CreateClient() + { + return new BTCPayServerClient(parent.PayTester.ServerUri, RegisterDetails.Email, RegisterDetails.Password); + } + public async Task CreateClient(params string[] permissions) { var manageController = parent.PayTester.GetController(UserId, StoreId, IsAdmin); diff --git a/BTCPayServer/Controllers/RestApi/ApiKeysController.cs b/BTCPayServer/Controllers/RestApi/ApiKeysController.cs index fff308122..95324ebf9 100644 --- a/BTCPayServer/Controllers/RestApi/ApiKeysController.cs +++ b/BTCPayServer/Controllers/RestApi/ApiKeysController.cs @@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Mvc; namespace BTCPayServer.Controllers.RestApi { [ApiController] - [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKey)] public class ApiKeysController : ControllerBase { private readonly APIKeyRepository _apiKeyRepository; @@ -33,7 +33,7 @@ namespace BTCPayServer.Controllers.RestApi } [HttpDelete("~/api/v1/api-keys/current")] - [Authorize(Policy = Policies.Unrestricted, AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] + [Authorize(Policy = Policies.Unrestricted, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] public async Task> RevokeKey() { ControllerContext.HttpContext.GetAPIKey(out var apiKey); diff --git a/BTCPayServer/Controllers/RestApi/UsersController.cs b/BTCPayServer/Controllers/RestApi/UsersController.cs index 301d9df46..25711540a 100644 --- a/BTCPayServer/Controllers/RestApi/UsersController.cs +++ b/BTCPayServer/Controllers/RestApi/UsersController.cs @@ -82,7 +82,7 @@ namespace BTCPayServer.Controllers.RestApi // Even if subscription are unlocked, it is forbidden to create admin unauthenticated if (anyAdmin && request.IsAdministrator is true && !isAuth) - return Forbid(AuthenticationSchemes.ApiKeyOrBasic); + return Forbid(AuthenticationSchemes.ApiKey, AuthenticationSchemes.Basic); // You are de-facto admin if there is no other admin, else you need to be auth and pass policy requirements bool isAdmin = anyAdmin ? (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanModifyServerSettings))).Succeeded && (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.Unrestricted))).Succeeded @@ -90,14 +90,14 @@ namespace BTCPayServer.Controllers.RestApi : true; // You need to be admin to create an admin if (request.IsAdministrator is true && !isAdmin) - return Forbid(AuthenticationSchemes.ApiKeyOrBasic); + return Forbid(AuthenticationSchemes.ApiKey, AuthenticationSchemes.Basic); if (!isAdmin && policies.LockSubscription) { // If we are not admin and subscriptions are locked, we need to check the Policies.CanCreateUser.Key permission var canCreateUser = (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanCreateUser))).Succeeded; if (!isAuth || !canCreateUser) - return Forbid(AuthenticationSchemes.ApiKeyOrBasic); + return Forbid(AuthenticationSchemes.ApiKey, AuthenticationSchemes.Basic); } var user = new ApplicationUser diff --git a/BTCPayServer/Security/AuthenticationSchemes.cs b/BTCPayServer/Security/AuthenticationSchemes.cs index e5d8683f9..5f34a7caa 100644 --- a/BTCPayServer/Security/AuthenticationSchemes.cs +++ b/BTCPayServer/Security/AuthenticationSchemes.cs @@ -6,6 +6,6 @@ public const string Bitpay = "Bitpay"; public const string ApiKey = "GreenfieldApiKey"; public const string Basic = "Basic"; - public const string ApiKeyOrBasic = "Basic,GreenfieldApiKey"; + public const string ApiKeyOrBasic = ApiKey + "," + Basic; } } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json index 8d7dfb820..7d167d9d9 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json @@ -209,8 +209,7 @@ }, "security": [ { - "GreenField Authentication": [], - "Basic": [] + "GreenField Authentication": [] } ] }, @@ -234,8 +233,7 @@ }, "security": [ { - "GreenField Authentication": [ "unrestricted" ], - "Basic": [] + "GreenField Authentication": [ "unrestricted" ] } ] } From 98a48cd0a5366bc39f89e489af4490ae6ae2b9a9 Mon Sep 17 00:00:00 2001 From: Kukks Date: Fri, 20 Mar 2020 17:37:39 +0100 Subject: [PATCH 316/810] fix swagger validation test --- BTCPayServer.Tests/UnitTest1.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index e43973286..fadd56dd7 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -102,6 +102,13 @@ namespace BTCPayServer.Tests var schema = JSchema.Parse(await resp.Content.ReadAsStringAsync()); IList errors; bool valid = swagger.IsValid(schema, out errors); + //the schema is not fully compliant to the spec. We ARE allowed to have multiple security schemas. + if (!valid && errors.Count == 1 && errors.Any(error => + error.Path == "components.securitySchemes.Basic" && error.ErrorType == ErrorType.OneOf)) + { + errors = new List(); + valid = true; + } Assert.Empty(errors); Assert.True(valid); } From e48e8c34d9ec25212fc07a9e75f32b3568930d78 Mon Sep 17 00:00:00 2001 From: Kukks Date: Fri, 20 Mar 2020 17:59:14 +0100 Subject: [PATCH 317/810] fix tst --- BTCPayServer.Tests/GreenfieldAPITests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 5c1129da5..6702e1081 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -116,14 +116,14 @@ namespace BTCPayServer.Tests user1Acc.UserId = user1.Id; user1Acc.IsAdmin = false; var user1Client = await user1Acc.CreateClient(Policies.CanModifyServerSettings); - var user1ClientBasic = await user1Acc.CreateClient(); + // User1 trying to get server management would still fail to create user await AssertHttpError(403, async () => await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" })); // User1 should be able to create user if subscription unlocked await settings.UpdateSetting(new PoliciesSettings() { LockSubscription = false }); await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" }); - await user1ClientBasic.CreateUser(new CreateApplicationUserRequest() { Email = "testaa8@gmail.com", Password = "afewfoiewiou" }); + // But not an admin await AssertHttpError(403, async () => await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "admin8@gmail.com", Password = "afewfoiewiou", IsAdministrator = true })); } From f3d5cf362225f9a799cd92d7c007b1c3871f38d8 Mon Sep 17 00:00:00 2001 From: rockstardev Date: Sat, 21 Mar 2020 12:23:02 -0500 Subject: [PATCH 318/810] Additional logging and tweaking for CanManageWallet test --- BTCPayServer.Tests/SeleniumTester.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index efade2146..58f02550f 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -127,9 +127,10 @@ namespace BTCPayServer.Tests Driver.FindElement(By.Id("import-from-btn")).ForceClick(); Driver.FindElement(By.Id("nbxplorergeneratewalletbtn")).ForceClick(); Driver.WaitForElement(By.Id("ExistingMnemonic")).SendKeys(seed); - SetCheckbox(Driver.FindElement(By.Id("SavePrivateKeys")), privkeys); - SetCheckbox(Driver.FindElement(By.Id("ImportKeysToRPC")), importkeys); - Driver.FindElement(By.Id("btn-generate")).ForceClick(); + SetCheckbox(Driver.WaitForElement(By.Id("SavePrivateKeys")), privkeys); + SetCheckbox(Driver.WaitForElement(By.Id("ImportKeysToRPC")), importkeys); + Logs.Tester.LogInformation("Trying to click btn-generate"); + Driver.WaitForElement(By.Id("btn-generate")).ForceClick(); AssertHappyMessage(); if (string.IsNullOrEmpty(seed)) { @@ -253,13 +254,14 @@ namespace BTCPayServer.Tests if (value != element.Selected) { + Logs.Tester.LogInformation("SetCheckbox recursion, trying to click again"); SetCheckbox(element, value); } } - public void SetCheckbox(SeleniumTester s, string inputName, bool value) + public void SetCheckbox(SeleniumTester s, string checkboxId, bool value) { - SetCheckbox(s.Driver.FindElement(By.Id(inputName)), value); + SetCheckbox(s.Driver.WaitForElement(By.Id(checkboxId)), value); } public void ScrollToElement(IWebElement element) From 805e1f53b3c3dba34da71318dffada410105b161 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Mon, 23 Mar 2020 15:46:54 +0900 Subject: [PATCH 319/810] Test Wallet Receive, Send with NBX, Coin selection --- BTCPayServer.Tests/SeleniumTester.cs | 12 ++- BTCPayServer.Tests/SeleniumTests.cs | 85 ++++++++++++++----- BTCPayServer/Data/StoreDataExtensions.cs | 2 + .../Views/Wallets/CoinSelection.cshtml | 8 +- .../Views/Wallets/WalletReceive.cshtml | 2 +- BTCPayServer/Views/Wallets/WalletSend.cshtml | 8 +- 6 files changed, 87 insertions(+), 30 deletions(-) diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index 58f02550f..259e8a417 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -121,7 +121,7 @@ namespace BTCPayServer.Tests return (usr, Driver.FindElement(By.Id("Id")).GetAttribute("value")); } - public string GenerateWallet(string cryptoCode = "BTC", string seed = "", bool importkeys = false, bool privkeys = false) + public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool importkeys = false, bool privkeys = false) { Driver.FindElement(By.Id($"Modify{cryptoCode}")).ForceClick(); Driver.FindElement(By.Id("import-from-btn")).ForceClick(); @@ -138,7 +138,7 @@ namespace BTCPayServer.Tests } Driver.FindElement(By.Id("Confirm")).ForceClick(); AssertHappyMessage(); - return seed; + return new Mnemonic(seed); } public void AddDerivationScheme(string cryptoCode = "BTC", string derivationScheme = "xpub661MyMwAqRbcGABgHMUXDzPzH1tU7eZaAaJQXhDXsSxsqyQzQeU6kznNfSuAyqAK9UaWSaZaMFdNiY5BCF4zBPAzSnwfUAwUhwttuAKwfRX-[legacy]") @@ -339,6 +339,14 @@ namespace BTCPayServer.Tests } + public void GoToWalletSend(WalletId walletId) + { + Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, $"wallets/{walletId}/send")); + } + internal void GoToWalletReceive(WalletId walletId) + { + Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, $"wallets/{walletId}/receive")); + } } } diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 5b072bdca..65087de66 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using System.Text.RegularExpressions; using BTCPayServer.Models; using NBitcoin.Payment; +using BTCPayServer.Controllers; namespace BTCPayServer.Tests { @@ -417,7 +418,54 @@ namespace BTCPayServer.Tests s.Driver.Quit(); } } - + + + [Fact(Timeout = TestTimeout)] + public async Task CanUseCoinSelection() + { + using (var s = SeleniumTester.Create()) + { + await s.StartAsync(); + var userId = s.RegisterNewUser(true); + var storeId = s.CreateNewStore().storeId; + s.GenerateWallet("BTC", "", false, true); + var walletId = new WalletId(storeId, "BTC"); + s.GoToWalletReceive(walletId); + s.Driver.FindElement(By.Id("generateButton")).Click(); + var addressStr = s.Driver.FindElement(By.Id("vue-address")).GetProperty("value"); + var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)s.Server.NetworkProvider.GetNetwork("BTC")).NBitcoinNetwork); + await s.Server.ExplorerNode.GenerateAsync(1); + for (int i = 0; i < 6; i++) + { + await s.Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(1.0m)); + } + var targetTx = await s.Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(1.2m)); + var tx = await s.Server.ExplorerNode.GetRawTransactionAsync(targetTx); + var spentOutpoint = new OutPoint(targetTx, tx.Outputs.FindIndex(txout => txout.Value == Money.Coins(1.2m))); + await s.Server.ExplorerNode.GenerateAsync(1); + s.GoToWalletSend(walletId); + s.Driver.FindElement(By.Id("advancedSettings")).Click(); + s.Driver.FindElement(By.Id("toggleInputSelection")).Click(); + s.Driver.WaitForElement(By.Id(spentOutpoint.ToString())); + await Task.Delay(1000); + var el = s.Driver.FindElement(By.Id(spentOutpoint.ToString())); + s.Driver.FindElement(By.Id(spentOutpoint.ToString())).Click(); + var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest); + SetTransactionOutput(s, 0, bob, 0.3m); + s.Driver.FindElement(By.Id("SendMenu")).Click(); + s.Driver.FindElement(By.Id("spendWithNBxplorer")).Click(); + s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick(); + var happyElement = s.AssertHappyMessage(); + var happyText = happyElement.Text; + var txid = Regex.Match(happyText, @"\((.*)\)").Groups[1].Value; + + tx = await s.Server.ExplorerNode.GetRawTransactionAsync(new uint256(txid)); + Assert.Single(tx.Inputs); + Assert.Equal(spentOutpoint, tx.Inputs[0].PrevOut); + } + } + + [Fact(Timeout = TestTimeout)] public async Task CanManageWallet() { @@ -488,7 +536,7 @@ namespace BTCPayServer.Tests var mnemonic = s.GenerateWallet("BTC", "", true, true); //lets import and save private keys - var root = new Mnemonic(mnemonic).DeriveExtKey(); + var root = mnemonic.DeriveExtKey(); invoiceId = s.CreateInvoice(storeId.storeId); invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice( invoiceId); address = invoice.EntityToDTO().Addresses["BTC"]; @@ -518,18 +566,18 @@ namespace BTCPayServer.Tests Assert.Contains(tx.ToString(), s.Driver.PageSource); - void SignWith(string signingSource) + void SignWith(Mnemonic signingSource) { // Send to bob s.Driver.FindElement(By.Id("WalletSend")).Click(); var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest); - SetTransactionOutput(0, bob, 1); + SetTransactionOutput(s, 0, bob, 1); s.Driver.ScrollTo(By.Id("SendMenu")); s.Driver.FindElement(By.Id("SendMenu")).ForceClick(); s.Driver.FindElement(By.CssSelector("button[value=seed]")).Click(); // Input the seed - s.Driver.FindElement(By.Id("SeedOrKey")).SendKeys(signingSource + Keys.Enter); + s.Driver.FindElement(By.Id("SeedOrKey")).SendKeys(signingSource.ToString() + Keys.Enter); // Broadcast Assert.Contains(bob.ToString(), s.Driver.PageSource); @@ -537,19 +585,6 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick(); Assert.Equal(walletTransactionLink, s.Driver.Url); } - - void SetTransactionOutput(int index, BitcoinAddress dest, decimal amount, bool subtract = false) - { - s.Driver.FindElement(By.Id($"Outputs_{index}__DestinationAddress")).SendKeys(dest.ToString()); - var amountElement = s.Driver.FindElement(By.Id($"Outputs_{index}__Amount")); - amountElement.Clear(); - amountElement.SendKeys(amount.ToString()); - var checkboxElement = s.Driver.FindElement(By.Id($"Outputs_{index}__SubtractFeesFromOutput")); - if (checkboxElement.Selected != subtract) - { - checkboxElement.Click(); - } - } SignWith(mnemonic); @@ -558,7 +593,7 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("WalletSend")).Click(); var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest); - SetTransactionOutput(0, jack, 0.01m); + SetTransactionOutput(s, 0, jack, 0.01m); s.Driver.ScrollTo(By.Id("SendMenu")); s.Driver.FindElement(By.Id("SendMenu")).ForceClick(); @@ -589,5 +624,17 @@ namespace BTCPayServer.Tests } } + void SetTransactionOutput(SeleniumTester s, int index, BitcoinAddress dest, decimal amount, bool subtract = false) + { + s.Driver.FindElement(By.Id($"Outputs_{index}__DestinationAddress")).SendKeys(dest.ToString()); + var amountElement = s.Driver.FindElement(By.Id($"Outputs_{index}__Amount")); + amountElement.Clear(); + amountElement.SendKeys(amount.ToString()); + var checkboxElement = s.Driver.FindElement(By.Id($"Outputs_{index}__SubtractFeesFromOutput")); + if (checkboxElement.Selected != subtract) + { + checkboxElement.Click(); + } + } } } diff --git a/BTCPayServer/Data/StoreDataExtensions.cs b/BTCPayServer/Data/StoreDataExtensions.cs index 59f7f00ef..a0f5da62a 100644 --- a/BTCPayServer/Data/StoreDataExtensions.cs +++ b/BTCPayServer/Data/StoreDataExtensions.cs @@ -67,6 +67,8 @@ namespace BTCPayServer.Data public static IEnumerable GetSupportedPaymentMethods(this StoreData storeData, BTCPayNetworkProvider networks) { + if (storeData == null) + throw new ArgumentNullException(nameof(storeData)); networks = networks.UnfilteredNetworks; #pragma warning disable CS0618 bool btcReturned = false; diff --git a/BTCPayServer/Views/Wallets/CoinSelection.cshtml b/BTCPayServer/Views/Wallets/CoinSelection.cshtml index ad1ddbf8a..3c09ac781 100644 --- a/BTCPayServer/Views/Wallets/CoinSelection.cshtml +++ b/BTCPayServer/Views/Wallets/CoinSelection.cshtml @@ -25,12 +25,12 @@ :key="item.outpoint" v-bind:class="{ 'alert-success': item.selected }" v-on:click="toggleItem($event, item, !item.selected)"> - {{item.outpoint}} -
    + {{item.outpoint}} +
    i - Receive @Model.CryptoCode - + } else { diff --git a/BTCPayServer/Views/Wallets/WalletSend.cshtml b/BTCPayServer/Views/Wallets/WalletSend.cshtml index 38b709fce..5a28dc7dc 100644 --- a/BTCPayServer/Views/Wallets/WalletSend.cshtml +++ b/BTCPayServer/Views/Wallets/WalletSend.cshtml @@ -43,7 +43,7 @@
    @@ -144,7 +144,7 @@ }
    -
    @@ -167,7 +167,7 @@
    }
    - +
    @@ -186,7 +186,7 @@ } @if (Model.NBXSeedAvailable) { - + }
    From d57fdd4785681a1cc4f0b20846f3088b61878fe1 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Mon, 23 Mar 2020 17:19:19 +0900 Subject: [PATCH 320/810] Remove useless delay in tests --- BTCPayServer.Tests/SeleniumTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 65087de66..33244e135 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -447,7 +447,6 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("advancedSettings")).Click(); s.Driver.FindElement(By.Id("toggleInputSelection")).Click(); s.Driver.WaitForElement(By.Id(spentOutpoint.ToString())); - await Task.Delay(1000); var el = s.Driver.FindElement(By.Id(spentOutpoint.ToString())); s.Driver.FindElement(By.Id(spentOutpoint.ToString())).Click(); var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest); From 56ba834ca28581788127a08cd19f5255a8ec3854 Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 23 Mar 2020 14:23:23 +0100 Subject: [PATCH 321/810] Consolidate auth into one --- .../Controllers/RestApi/ApiKeysController.cs | 4 +- .../RestApi/TestApiKeyController.cs | 14 ++-- .../Controllers/RestApi/UsersController.cs | 10 +-- BTCPayServer/Hosting/BTCPayServerServices.cs | 3 +- .../APIKeys/APIKeyAuthenticationHandler.cs | 54 +++++++++++++-- .../Security/APIKeys/APIKeyExtensions.cs | 2 +- .../Security/AuthenticationSchemes.cs | 4 +- .../Basic/BasicAuthenticationHandler.cs | 65 ------------------- .../Basic/BasicAuthenticationOptions.cs | 8 --- .../Security/Basic/BasicExtensions.cs | 17 ----- 10 files changed, 67 insertions(+), 114 deletions(-) delete mode 100644 BTCPayServer/Security/Basic/BasicAuthenticationHandler.cs delete mode 100644 BTCPayServer/Security/Basic/BasicAuthenticationOptions.cs delete mode 100644 BTCPayServer/Security/Basic/BasicExtensions.cs diff --git a/BTCPayServer/Controllers/RestApi/ApiKeysController.cs b/BTCPayServer/Controllers/RestApi/ApiKeysController.cs index 95324ebf9..3fd79e781 100644 --- a/BTCPayServer/Controllers/RestApi/ApiKeysController.cs +++ b/BTCPayServer/Controllers/RestApi/ApiKeysController.cs @@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Mvc; namespace BTCPayServer.Controllers.RestApi { [ApiController] - [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)] public class ApiKeysController : ControllerBase { private readonly APIKeyRepository _apiKeyRepository; @@ -33,7 +33,7 @@ namespace BTCPayServer.Controllers.RestApi } [HttpDelete("~/api/v1/api-keys/current")] - [Authorize(Policy = Policies.Unrestricted, AuthenticationSchemes = AuthenticationSchemes.ApiKey)] + [Authorize(Policy = Policies.Unrestricted, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] public async Task> RevokeKey() { ControllerContext.HttpContext.GetAPIKey(out var apiKey); diff --git a/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs b/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs index 3673b4fb9..a3c89c67c 100644 --- a/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs +++ b/BTCPayServer/Controllers/RestApi/TestApiKeyController.cs @@ -14,7 +14,7 @@ namespace BTCPayServer.Controllers.RestApi /// [Route("api/test/apikey")] [ApiController] - [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)] public class TestApiKeyController : ControllerBase { private readonly UserManager _userManager; @@ -27,28 +27,28 @@ namespace BTCPayServer.Controllers.RestApi } [HttpGet("me/id")] - [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] + [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] public string GetCurrentUserId() { return _userManager.GetUserId(User); } [HttpGet("me")] - [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] + [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] public async Task GetCurrentUser() { return await _userManager.GetUserAsync(User); } [HttpGet("me/is-admin")] - [Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] + [Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] public bool AmIAnAdmin() { return true; } [HttpGet("me/stores")] - [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] + [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] public StoreData[] GetCurrentUserStores() { return this.HttpContext.GetStoresData(); @@ -56,7 +56,7 @@ namespace BTCPayServer.Controllers.RestApi [HttpGet("me/stores/{storeId}/can-view")] [Authorize(Policy = Policies.CanViewStoreSettings, - AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] public bool CanViewStore(string storeId) { return true; @@ -64,7 +64,7 @@ namespace BTCPayServer.Controllers.RestApi [HttpGet("me/stores/{storeId}/can-edit")] [Authorize(Policy = Policies.CanModifyStoreSettings, - AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] public bool CanEditStore(string storeId) { return true; diff --git a/BTCPayServer/Controllers/RestApi/UsersController.cs b/BTCPayServer/Controllers/RestApi/UsersController.cs index 25711540a..cd2140963 100644 --- a/BTCPayServer/Controllers/RestApi/UsersController.cs +++ b/BTCPayServer/Controllers/RestApi/UsersController.cs @@ -20,7 +20,7 @@ using BTCPayServer.Client; namespace BTCPayServer.Controllers.RestApi { [ApiController] - [Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)] public class UsersController : ControllerBase { private readonly UserManager _userManager; @@ -52,7 +52,7 @@ namespace BTCPayServer.Controllers.RestApi _authorizationService = authorizationService; } - [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKeyOrBasic)] + [Authorize(Policy = Policies.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpGet("~/api/v1/users/me")] public async Task> GetCurrentUser() { @@ -82,7 +82,7 @@ namespace BTCPayServer.Controllers.RestApi // Even if subscription are unlocked, it is forbidden to create admin unauthenticated if (anyAdmin && request.IsAdministrator is true && !isAuth) - return Forbid(AuthenticationSchemes.ApiKey, AuthenticationSchemes.Basic); + return Forbid(AuthenticationSchemes.Greenfield); // You are de-facto admin if there is no other admin, else you need to be auth and pass policy requirements bool isAdmin = anyAdmin ? (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanModifyServerSettings))).Succeeded && (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.Unrestricted))).Succeeded @@ -90,14 +90,14 @@ namespace BTCPayServer.Controllers.RestApi : true; // You need to be admin to create an admin if (request.IsAdministrator is true && !isAdmin) - return Forbid(AuthenticationSchemes.ApiKey, AuthenticationSchemes.Basic); + return Forbid(AuthenticationSchemes.Greenfield); if (!isAdmin && policies.LockSubscription) { // If we are not admin and subscriptions are locked, we need to check the Policies.CanCreateUser.Key permission var canCreateUser = (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanCreateUser))).Succeeded; if (!isAuth || !canCreateUser) - return Forbid(AuthenticationSchemes.ApiKey, AuthenticationSchemes.Basic); + return Forbid(AuthenticationSchemes.Greenfield); } var user = new ApplicationUser diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index c32ea9907..07d1282e7 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -286,8 +286,7 @@ namespace BTCPayServer.Hosting services.AddAuthentication() .AddCookie() .AddBitpayAuthentication() - .AddAPIKeyAuthentication() - .AddBasicAuthentication(); + .AddAPIKeyAuthentication(); } public static IApplicationBuilder UsePayServer(this IApplicationBuilder app) diff --git a/BTCPayServer/Security/APIKeys/APIKeyAuthenticationHandler.cs b/BTCPayServer/Security/APIKeys/APIKeyAuthenticationHandler.cs index 07eff055d..1ef14a1f3 100644 --- a/BTCPayServer/Security/APIKeys/APIKeyAuthenticationHandler.cs +++ b/BTCPayServer/Security/APIKeys/APIKeyAuthenticationHandler.cs @@ -2,12 +2,12 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; +using System.Text; using System.Text.Encodings.Web; using System.Threading.Tasks; using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Security.Bitpay; -using BTCPayServer.Services.Stores; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; @@ -19,6 +19,8 @@ namespace BTCPayServer.Security.APIKeys { private readonly APIKeyRepository _apiKeyRepository; private readonly IOptionsMonitor _identityOptions; + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; public APIKeyAuthenticationHandler( APIKeyRepository apiKeyRepository, @@ -26,13 +28,28 @@ namespace BTCPayServer.Security.APIKeys IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, - ISystemClock clock) : base(options, logger, encoder, clock) + ISystemClock clock, + SignInManager signInManager, + UserManager userManager) : base(options, logger, encoder, clock) { _apiKeyRepository = apiKeyRepository; _identityOptions = identityOptions; + _signInManager = signInManager; + _userManager = userManager; } protected override async Task HandleAuthenticateAsync() + { + var res = await HandleApiKeyAuthenticateResult(); + if (res.None) + { + return await HandleBasicAuthenticateAsync(); + } + + return res; + } + + private async Task HandleApiKeyAuthenticateResult() { if (!Context.Request.HttpContext.GetAPIKey(out var apiKey) || string.IsNullOrEmpty(apiKey)) return AuthenticateResult.NoResult(); @@ -46,9 +63,38 @@ namespace BTCPayServer.Security.APIKeys List claims = new List(); claims.Add(new Claim(_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, key.UserId)); - claims.AddRange(Permission.ToPermissions(key.Permissions).Select(permission => new Claim(APIKeyConstants.ClaimTypes.Permission, permission.ToString()))); + claims.AddRange(Permission.ToPermissions(key.Permissions).Select(permission => + new Claim(APIKeyConstants.ClaimTypes.Permission, permission.ToString()))); return AuthenticateResult.Success(new AuthenticationTicket( - new ClaimsPrincipal(new ClaimsIdentity(claims, APIKeyConstants.AuthenticationType)), APIKeyConstants.AuthenticationType)); + new ClaimsPrincipal(new ClaimsIdentity(claims, APIKeyConstants.AuthenticationType)), + APIKeyConstants.AuthenticationType)); + } + + private async Task HandleBasicAuthenticateAsync() + { + string authHeader = Context.Request.Headers["Authorization"]; + + if (authHeader == null || !authHeader.StartsWith("Basic ")) return AuthenticateResult.NoResult(); + var encodedUsernamePassword = authHeader.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries)[1]?.Trim(); + var decodedUsernamePassword = + Encoding.UTF8.GetString(Convert.FromBase64String(encodedUsernamePassword)).Split(':'); + var username = decodedUsernamePassword[0]; + var password = decodedUsernamePassword[1]; + + var result = await _signInManager.PasswordSignInAsync(username, password, true, true); + if (!result.Succeeded) return AuthenticateResult.Fail(result.ToString()); + + var user = await _userManager.FindByNameAsync(username); + var claims = new List() + { + new Claim(_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, user.Id), + new Claim(APIKeyConstants.ClaimTypes.Permission, + Permission.Create(Policies.Unrestricted).ToString()) + }; + + return AuthenticateResult.Success(new AuthenticationTicket( + new ClaimsPrincipal(new ClaimsIdentity(claims, APIKeyConstants.AuthenticationType)), + APIKeyConstants.AuthenticationType)); } } } diff --git a/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs b/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs index d1b6a391a..8671b6992 100644 --- a/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs +++ b/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs @@ -26,7 +26,7 @@ namespace BTCPayServer.Security.APIKeys public static AuthenticationBuilder AddAPIKeyAuthentication(this AuthenticationBuilder builder) { - builder.AddScheme(AuthenticationSchemes.ApiKey, + builder.AddScheme(AuthenticationSchemes.Greenfield, o => { }); return builder; } diff --git a/BTCPayServer/Security/AuthenticationSchemes.cs b/BTCPayServer/Security/AuthenticationSchemes.cs index 5f34a7caa..de87c4116 100644 --- a/BTCPayServer/Security/AuthenticationSchemes.cs +++ b/BTCPayServer/Security/AuthenticationSchemes.cs @@ -4,8 +4,6 @@ { public const string Cookie = "Identity.Application"; public const string Bitpay = "Bitpay"; - public const string ApiKey = "GreenfieldApiKey"; - public const string Basic = "Basic"; - public const string ApiKeyOrBasic = ApiKey + "," + Basic; + public const string Greenfield = "Greenfield"; } } diff --git a/BTCPayServer/Security/Basic/BasicAuthenticationHandler.cs b/BTCPayServer/Security/Basic/BasicAuthenticationHandler.cs deleted file mode 100644 index 34be18070..000000000 --- a/BTCPayServer/Security/Basic/BasicAuthenticationHandler.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Text; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using BTCPayServer.Client; -using BTCPayServer.Data; -using BTCPayServer.Security.APIKeys; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace BTCPayServer.Security.Basic -{ - public class BasicAuthenticationHandler : AuthenticationHandler - { - private readonly IOptionsMonitor _identityOptions; - private readonly SignInManager _signInManager; - private readonly UserManager _userManager; - - public BasicAuthenticationHandler( - - IOptionsMonitor identityOptions, - SignInManager signInManager, - UserManager userManager, - IOptionsMonitor options, - ILoggerFactory logger, - UrlEncoder encoder, - ISystemClock clock) : base(options, logger, encoder, clock) - { - _identityOptions = identityOptions; - _signInManager = signInManager; - _userManager = userManager; - } - - protected override async Task HandleAuthenticateAsync() - { - - string authHeader = Context.Request.Headers["Authorization"]; - - if (authHeader == null || !authHeader.StartsWith("Basic ")) return AuthenticateResult.NoResult(); - var encodedUsernamePassword = authHeader.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries)[1]?.Trim(); - var decodedUsernamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(encodedUsernamePassword)).Split(':'); - var username = decodedUsernamePassword[0]; - var password = decodedUsernamePassword[1]; - - var result = await _signInManager.PasswordSignInAsync(username, password, true, true); - if (!result.Succeeded) return AuthenticateResult.Fail(result.ToString()); - - var user = await _userManager.FindByNameAsync(username); - var claims = new List() - { - new Claim(_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, user.Id), - new Claim(APIKeyConstants.ClaimTypes.Permission, Permission.Create(Policies.Unrestricted).ToString()) - }; - - return AuthenticateResult.Success(new AuthenticationTicket( - new ClaimsPrincipal(new ClaimsIdentity(claims, APIKeyConstants.AuthenticationType)), APIKeyConstants.AuthenticationType)); - - } - } -} diff --git a/BTCPayServer/Security/Basic/BasicAuthenticationOptions.cs b/BTCPayServer/Security/Basic/BasicAuthenticationOptions.cs deleted file mode 100644 index f326d66e8..000000000 --- a/BTCPayServer/Security/Basic/BasicAuthenticationOptions.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Microsoft.AspNetCore.Authentication; - -namespace BTCPayServer.Security.Basic -{ - public class BasicAuthenticationOptions : AuthenticationSchemeOptions - { - } -} diff --git a/BTCPayServer/Security/Basic/BasicExtensions.cs b/BTCPayServer/Security/Basic/BasicExtensions.cs deleted file mode 100644 index fd51395b8..000000000 --- a/BTCPayServer/Security/Basic/BasicExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using BTCPayServer.Security.Basic; -using Microsoft.AspNetCore.Authentication; - -namespace BTCPayServer.Security.APIKeys -{ - public static class BasicExtensions - { - - public static AuthenticationBuilder AddBasicAuthentication(this AuthenticationBuilder builder) - { - builder.AddScheme(AuthenticationSchemes.Basic, - o => { }); - return builder; - } - - } -} From 7899c2d5c55ec0e340c7c55b00541f16ccfce876 Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 23 Mar 2020 21:18:02 +0100 Subject: [PATCH 322/810] fix test --- BTCPayServer.Tests/GreenfieldAPITests.cs | 4 ++-- BTCPayServer/Controllers/RestApi/ApiKeysController.cs | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 6702e1081..ca6ef6109 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -46,13 +46,13 @@ namespace BTCPayServer.Tests Assert.Single(apiKeyData.Permissions); //a client using Basic Auth has no business here - await AssertHttpError(401, async () => await clientBasic.GetCurrentAPIKeyInfo()); + await AssertHttpError(404, async () => await clientBasic.GetCurrentAPIKeyInfo()); //revoke current api key await client.RevokeCurrentAPIKeyInfo(); await AssertHttpError(401, async () => await client.GetCurrentAPIKeyInfo()); //a client using Basic Auth has no business here - await AssertHttpError(401, async () => await clientBasic.RevokeCurrentAPIKeyInfo()); + await AssertHttpError(404, async () => await clientBasic.RevokeCurrentAPIKeyInfo()); } diff --git a/BTCPayServer/Controllers/RestApi/ApiKeysController.cs b/BTCPayServer/Controllers/RestApi/ApiKeysController.cs index 3fd79e781..c0b2ab010 100644 --- a/BTCPayServer/Controllers/RestApi/ApiKeysController.cs +++ b/BTCPayServer/Controllers/RestApi/ApiKeysController.cs @@ -27,7 +27,10 @@ namespace BTCPayServer.Controllers.RestApi [HttpGet("~/api/v1/api-keys/current")] public async Task> GetKey() { - ControllerContext.HttpContext.GetAPIKey(out var apiKey); + if (!ControllerContext.HttpContext.GetAPIKey(out var apiKey)) + { + return NotFound(); + } var data = await _apiKeyRepository.GetKey(apiKey); return Ok(FromModel(data)); } @@ -36,7 +39,10 @@ namespace BTCPayServer.Controllers.RestApi [Authorize(Policy = Policies.Unrestricted, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] public async Task> RevokeKey() { - ControllerContext.HttpContext.GetAPIKey(out var apiKey); + if (!ControllerContext.HttpContext.GetAPIKey(out var apiKey)) + { + return NotFound(); + } await _apiKeyRepository.Remove(apiKey, _userManager.GetUserId(User)); return Ok(); } From 91924512e6e72fcb746eefeb6fa738d7e68de11b Mon Sep 17 00:00:00 2001 From: Kukks Date: Tue, 24 Mar 2020 07:53:54 +0100 Subject: [PATCH 323/810] Fix Link checker test --- BTCPayServer.Tests/UnitTest1.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index e43973286..632e25e60 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -122,12 +122,15 @@ namespace BTCPayServer.Tests var url = match.Groups[1].Value; try { - Assert.Equal(HttpStatusCode.OK, (await httpClient.GetAsync(url)).StatusCode); + using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(url)); + request.Headers.TryAddWithoutValidation("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"); + request.Headers.TryAddWithoutValidation("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0"); + Assert.Equal(HttpStatusCode.OK, (await httpClient.SendAsync(request)).StatusCode); Logs.Tester.LogInformation($"OK: {url} ({file})"); } - catch + catch(EqualException ex) { - Logs.Tester.LogInformation($"FAILED: {url} ({file})"); + Logs.Tester.LogInformation($"FAILED: {url} ({file}) {ex.Actual}"); throw; } } From 6df7ffd7e20f247a88b71a51f6c39a0443879b42 Mon Sep 17 00:00:00 2001 From: Kukks Date: Tue, 24 Mar 2020 08:55:32 +0100 Subject: [PATCH 324/810] lol windows phone --- BTCPayServer/Views/Manage/EnableAuthenticator.cshtml | 1 - 1 file changed, 1 deletion(-) diff --git a/BTCPayServer/Views/Manage/EnableAuthenticator.cshtml b/BTCPayServer/Views/Manage/EnableAuthenticator.cshtml index a0b4d6c49..b3738f149 100644 --- a/BTCPayServer/Views/Manage/EnableAuthenticator.cshtml +++ b/BTCPayServer/Views/Manage/EnableAuthenticator.cshtml @@ -9,7 +9,6 @@
  • Download a two-factor authenticator app like Microsoft Authenticator for - Windows Phone, Android and iOS or Google Authenticator for From f0f05acdfde075213c4f9d0cf4e0ecba13662d39 Mon Sep 17 00:00:00 2001 From: Kukks Date: Fri, 28 Feb 2020 10:10:07 +0100 Subject: [PATCH 325/810] Let 0 amount invoices pass through and allow email to be set when required but paid @ketominer @Askuwheteau @IAskuwheteau --- BTCPayServer/Services/Invoices/InvoiceEntity.cs | 6 +++++- BTCPayServer/Views/Invoice/Checkout-Body.cshtml | 4 ++-- BTCPayServer/Views/Invoice/Checkout.cshtml | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index ac82730fb..1b219f2d1 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -888,7 +888,11 @@ namespace BTCPayServer.Services.Invoices accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero); accounting.DueUncapped = accounting.TotalDue - accounting.Paid; accounting.NetworkFee = accounting.TotalDue - totalDueNoNetworkCost; - var minimumTotalDueSatoshi = Math.Max(1.0m, accounting.TotalDue.Satoshi * (1.0m - ((decimal)ParentEntity.PaymentTolerance / 100.0m))); + // If the total due is 0, there is no payment tolerance to calculate + var minimumTotalDueSatoshi = accounting.TotalDue.Satoshi == 0 + ? 0 + : Math.Max(1.0m, + accounting.TotalDue.Satoshi * (1.0m - ((decimal)ParentEntity.PaymentTolerance / 100.0m))); accounting.MinimumTotalDue = Money.Satoshis(minimumTotalDueSatoshi); return accounting; } diff --git a/BTCPayServer/Views/Invoice/Checkout-Body.cshtml b/BTCPayServer/Views/Invoice/Checkout-Body.cshtml index e872b32e3..8832deab4 100644 --- a/BTCPayServer/Views/Invoice/Checkout-Body.cshtml +++ b/BTCPayServer/Views/Invoice/Checkout-Body.cshtml @@ -155,7 +155,7 @@

  • -
    +
    {{$t("Contact and Refund Email")}} @@ -190,7 +190,7 @@
    -