Compare commits
81 Commits
v1.9.2-rc2
...
v1.10.2
Author | SHA1 | Date | |
---|---|---|---|
6d0f9120b8 | |||
aafb4a7f2a | |||
ae432ff237 | |||
cdc318c71a | |||
94d1cec8a9 | |||
c0bc19ea59 | |||
6f07714cd9 | |||
a9d2cac23c | |||
693b46126b | |||
bbff9710bf | |||
358e122775 | |||
3818468932 | |||
3d2554fbe1 | |||
4309603317 | |||
f733c9ea77 | |||
775ee01171 | |||
33ec790137 | |||
0c575c888c | |||
24f7e51e3a | |||
0a0cf97c55 | |||
16b988d097 | |||
5edc0ff6ef | |||
375b96e508 | |||
1e72b12074 | |||
4a6d52f78e | |||
35dd580e74 | |||
79836ef1de | |||
8cb06f9c6c | |||
215a36e7a9 | |||
247f6b86a5 | |||
a9d42f1e6a | |||
4e03c2523a | |||
418b476725 | |||
783e4ccb35 | |||
6b7fb55658 | |||
3d5361cd11 | |||
2c4349c630 | |||
3589417b58 | |||
55203e0b64 | |||
a918288e3b | |||
e183138d2c | |||
d3e42862ed | |||
8860eec254 | |||
97e7e60cea | |||
44aaf7acbb | |||
9b721fae27 | |||
c3f412e3bb | |||
ee738a29f0 | |||
6c6544bf9b | |||
3d57b944ca | |||
acf003b1b4 | |||
52e108d32f | |||
7b96f96025 | |||
8db5e7e043 | |||
25fb5c1293 | |||
37f0498def | |||
02110f93d7 | |||
195dfc2c47 | |||
541b6cf9eb | |||
2c26b77afc | |||
99bcec5597 | |||
781190a65d | |||
3763480280 | |||
6fad5ebedb | |||
0690194aa1 | |||
03b94e2be3 | |||
18e34b3cbe | |||
a0bb3ace61 | |||
920ad67633 | |||
8b8f72129c | |||
b9b11e722c | |||
eddd458744 | |||
439ea20a89 | |||
cec223c8e7 | |||
25cb188d00 | |||
31007a8d96 | |||
a4fa8db69b | |||
6193835ea1 | |||
0c78e9e4ac | |||
58c409e7fa | |||
9577eed524 |
.github
BTCPayServer.Abstractions
BTCPayServer.Client
BTCPayServer.Client.csprojBTCPayServerClient.ServerInfo.csBTCPayServerClient.StoreUsers.csBTCPayServerClient.cs
Models
Permissions.csBTCPayServer.Common
BTCPayServer.Data
BTCPayServer.Tests
AltcoinTests
Checkoutv2Tests.csCrowdfundTests.csFastTests.csGreenfieldAPITests.csPOSTests.csSeleniumTester.csSeleniumTests.csServerTester.csTestAccount.csTestUtils.csThirdPartyTests.csUnitTest1.csdocker-compose.altcoins.ymldocker-compose.ymlBTCPayServer
BTCPayServer.csprojSearchString.cs
Components
AppTopItems
MainNav
StoreLightningBalance
StoreRecentInvoices
StoreRecentTransactions
StoreSelector
StoreWalletBalance
TruncateCenter
WalletNav
Controllers
GreenField
GreenfieldAppsController.csGreenfieldInvoiceController.csGreenfieldLightningNodeApiController.csGreenfieldPullPaymentController.csGreenfieldServerRolesController.csGreenfieldStoreAutomatedLightningPayoutProcessorsController.csGreenfieldStoreAutomatedOnChainPayoutProcessorsController.csGreenfieldStoreOnChainWalletsController.csGreenfieldStoreRolesController.csGreenfieldStoreUsersController.csGreenfieldStoreWebhooksController.csGreenfieldStoresController.csLocalBTCPayServerClient.cs
UIHomeController.csUIInvoiceController.UI.csUIInvoiceController.csUILNURLController.csUIPaymentRequestController.csUIServerController.Roles.csUIStoresController.Onchain.csUIStoresController.Roles.csUIStoresController.csUIUserStoresController.csUIWalletsController.PSBT.csUIWalletsController.csData
EventAggregator.csExtensions.csExtensions
Forms
FieldValueMirror.csFormDataExtensions.csHtmlInputFormProvider.csHtmlSelectFormProvider.csHtmlTextareaFormProvider.csUIFormsController.cs
HostedServices
AppInventoryUpdaterHostedService.csBitpayIPNSender.csCleanupWebhookDeliveriesTask.csIPeriodicTask.csInvoiceWatcher.csPeriodicTaskLauncherHostedService.csScheduledTask.csWebhookSender.cs
Hosting
Models
AppViewModels
InvoicingModels
ServerViewModels
StoreViewModels
CreateStoreViewModel.csEditWebhookViewModel.csGeneralSettingsViewModel.csStoreUsersViewModel.csWalletSetupRequest.cs
WalletViewModels
Payments
Bitcoin
LNURLPay
Lightning
PayoutProcessors
Plugins
Crowdfund
NFC
PayButton/Controllers
PointOfSale
Shopify
Security
Services
StorePolicies.csViews
Shared
Bitcoin
CreateOrEditRole.cshtmlCrowdfund/Public
Forms
LayoutHeadTheme.cshtmlLightning
LightningLikeMethodCheckout-v2.cshtmlLightningLikeMethodCheckout.cshtmlViewLightningLikePaymentData.cshtml
ListRoles.cshtmlPointOfSale/Public
PosData.cshtmlShopify
TemplateEditor.cshtmlUIApps
UIForms
UIHome
UIInvoice
CheckoutV2.cshtmlInvoice.cshtmlInvoiceReceipt.cshtmlListInvoices.cshtmlListInvoicesPaymentsPartial.cshtml_RefundModal.cshtml
UIPaymentRequest
UIPayoutProcessors
UIServer
UIStorePullPayments
UIStores
Dashboard.cshtmlGeneralSettings.cshtmlLightning.cshtmlLightningSettings.cshtmlModifyWebhook.cshtmlRates.cshtmlStoreNavPages.csStoreUsers.cshtml_GenerateWalletForm.cshtml_LayoutWalletSetup.cshtml_Nav.cshtml
UIUserStores
UIWallets
wwwroot
cart/js
checkout-v2
crowdfund
img
js
locales
main
swagger/v1
swagger.template.invoices.jsonswagger.template.serverinfo.jsonswagger.template.stores.jsonswagger.template.webhooks.json
vendor/vue-sortable
Build
Changelog.mdREADME.mdbtcpayserver.sln
2
.github/codeql/codeql-config.yml
vendored
Normal file
2
.github/codeql/codeql-config.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
paths-ignore:
|
||||
- 'BTCPayServer/wwwroot/vendor/**/*.js'
|
80
.github/workflows/codeql.yml
vendored
Normal file
80
.github/workflows/codeql.yml
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
# Allow running tests manually. Usefull if scan failure, or need to rescan before next scheduled date.
|
||||
workflow_dispatch:
|
||||
# We scan only on a schedule for now, can uncomment the following to scan on commit or PR merge later on if deemed appropriate.
|
||||
# push:
|
||||
# branches: [ "master" ]
|
||||
# pull_request:
|
||||
# branches: [ "master" ]
|
||||
schedule:
|
||||
# Scan every Monday 06:00 UTC.
|
||||
- cron: '0 6 * * 1'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript', 'csharp' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Use only 'java' to analyze code written in Java, Kotlin or both
|
||||
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Data.Common;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
@ -21,7 +22,6 @@ namespace BTCPayServer.Abstractions.Contracts
|
||||
}
|
||||
|
||||
public abstract T CreateContext();
|
||||
|
||||
class CustomNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator
|
||||
{
|
||||
#pragma warning disable EF1001 // Internal EF Core API usage.
|
||||
|
@ -69,7 +69,6 @@ public class Form
|
||||
if (!nameReturned.Add(fullName))
|
||||
{
|
||||
errors.Add($"Form contains duplicate field names '{fullName}'");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return errors.Count == 0;
|
||||
@ -86,15 +85,10 @@ public class Form
|
||||
thisPath.Add(field.Name);
|
||||
yield return (thisPath, field);
|
||||
}
|
||||
|
||||
foreach (var child in field.Fields)
|
||||
foreach (var descendant in GetAllFieldsCore(thisPath, field.Fields))
|
||||
{
|
||||
if (field.Constant)
|
||||
child.Constant = true;
|
||||
foreach (var descendant in GetAllFieldsCore(thisPath, field.Fields))
|
||||
{
|
||||
yield return descendant;
|
||||
}
|
||||
descendant.Field.Constant = field.Constant || descendant.Field.Constant;
|
||||
yield return descendant;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,8 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayServer.Abstractions.TagHelpers;
|
||||
|
||||
[HtmlTargetElement(Attributes = nameof(Permission))]
|
||||
[HtmlTargetElement(Attributes = "[permission]")]
|
||||
[HtmlTargetElement(Attributes = "[not-permission]" )]
|
||||
public class PermissionTagHelper : TagHelper
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
@ -21,16 +22,19 @@ public class PermissionTagHelper : TagHelper
|
||||
}
|
||||
|
||||
public string Permission { get; set; }
|
||||
public string NotPermission { get; set; }
|
||||
public string PermissionResource { get; set; }
|
||||
|
||||
|
||||
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
if (string.IsNullOrEmpty(Permission))
|
||||
if (string.IsNullOrEmpty(Permission) && string.IsNullOrEmpty(NotPermission))
|
||||
return;
|
||||
if (_httpContextAccessor.HttpContext is null)
|
||||
return;
|
||||
|
||||
var key = $"{Permission}_{PermissionResource}";
|
||||
var expectedResult = !string.IsNullOrEmpty(Permission);
|
||||
var key = $"{Permission??NotPermission}_{PermissionResource}";
|
||||
if (!_httpContextAccessor.HttpContext.Items.TryGetValue(key, out var o) ||
|
||||
o is not AuthorizationResult res)
|
||||
{
|
||||
@ -39,7 +43,7 @@ public class PermissionTagHelper : TagHelper
|
||||
Permission);
|
||||
_httpContextAccessor.HttpContext.Items.Add(key, res);
|
||||
}
|
||||
if (!res.Succeeded)
|
||||
if (expectedResult != res.Succeeded)
|
||||
{
|
||||
output.SuppressOutput();
|
||||
}
|
||||
|
@ -12,6 +12,8 @@
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<RepositoryUrl>https://github.com/btcpayserver/btcpayserver</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<Configurations>Debug;Release;Altcoins-Debug;Altcoins-Release</Configurations>
|
||||
<Platforms>AnyCPU</Platforms>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<Version Condition=" '$(Version)' == '' ">1.7.2</Version>
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
@ -11,5 +12,11 @@ namespace BTCPayServer.Client
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/server/info"), token);
|
||||
return await HandleResponse<ServerInfoData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<List<RoleData>> GetServerRoles(CancellationToken token = default)
|
||||
{
|
||||
using var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/server/roles"), token);
|
||||
return await HandleResponse<List<RoleData>>(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,13 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
public partial class BTCPayServerClient
|
||||
{
|
||||
public virtual async Task<List<RoleData>> GetStoreRoles(string storeId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
using var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/roles"), token);
|
||||
return await HandleResponse<List<RoleData>>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<IEnumerable<StoreUserData>> GetStoreUsers(string storeId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
|
@ -51,7 +51,8 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
if (message.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity)
|
||||
{
|
||||
var err = JsonConvert.DeserializeObject<Models.GreenfieldValidationError[]>(await message.Content.ReadAsStringAsync());
|
||||
var aa = await message.Content.ReadAsStringAsync();
|
||||
var err = JsonConvert.DeserializeObject<Models.GreenfieldValidationError[]>(aa);
|
||||
throw new GreenfieldValidationException(err);
|
||||
}
|
||||
if (message.StatusCode == System.Net.HttpStatusCode.Forbidden)
|
||||
|
@ -9,6 +9,7 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
RateThen,
|
||||
CurrentRate,
|
||||
OverpaidAmount,
|
||||
Fiat,
|
||||
Custom
|
||||
}
|
||||
@ -18,8 +19,13 @@ namespace BTCPayServer.Client.Models
|
||||
public string? Name { get; set; } = null;
|
||||
public string? PaymentMethod { get; set; }
|
||||
public string? Description { get; set; } = null;
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public RefundVariant? RefundVariant { get; set; }
|
||||
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal SubtractPercentage { get; set; }
|
||||
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? CustomAmount { get; set; }
|
||||
public string? CustomCurrency { get; set; }
|
||||
|
@ -16,6 +16,8 @@ namespace BTCPayServer.Client.Models
|
||||
|
||||
public string Website { get; set; }
|
||||
|
||||
public string SupportUrl { get; set; }
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public TimeSpan InvoiceExpiration { get; set; } = TimeSpan.FromMinutes(15);
|
||||
|
@ -1,3 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class StoreData : StoreBaseData
|
||||
@ -17,4 +19,12 @@ namespace BTCPayServer.Client.Models
|
||||
|
||||
public string Role { get; set; }
|
||||
}
|
||||
|
||||
public class RoleData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public List<string> Permissions { get; set; }
|
||||
public string Role { get; set; }
|
||||
public bool IsServerRole { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +51,10 @@ namespace BTCPayServer.Client.Models
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; }
|
||||
public bool IsPruned()
|
||||
{
|
||||
return DeliveryId is null;
|
||||
}
|
||||
public T ReadAs<T>()
|
||||
{
|
||||
var str = JsonConvert.SerializeObject(this, DefaultSerializerSettings);
|
||||
|
@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
@ -134,7 +136,7 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
static Permission()
|
||||
{
|
||||
Init();
|
||||
PolicyMap = Init();
|
||||
}
|
||||
|
||||
public static Permission Create(string policy, string scope = null)
|
||||
@ -235,11 +237,13 @@ namespace BTCPayServer.Client
|
||||
return subPolicies.Contains(subpolicy) || subPolicies.Any(s => ContainsPolicy(s, subpolicy));
|
||||
}
|
||||
|
||||
private static Dictionary<string, HashSet<string>> PolicyMap = new();
|
||||
public static ReadOnlyDictionary<string, HashSet<string>> PolicyMap { get; private set; }
|
||||
|
||||
|
||||
private static void Init()
|
||||
private static ReadOnlyDictionary<string, HashSet<string>> Init()
|
||||
{
|
||||
PolicyHasChild(Policies.CanModifyStoreSettings,
|
||||
var policyMap = new Dictionary<string, HashSet<string>>();
|
||||
PolicyHasChild(policyMap, Policies.CanModifyStoreSettings,
|
||||
Policies.CanManageCustodianAccounts,
|
||||
Policies.CanManagePullPayments,
|
||||
Policies.CanModifyInvoices,
|
||||
@ -248,25 +252,42 @@ namespace BTCPayServer.Client
|
||||
Policies.CanModifyPaymentRequests,
|
||||
Policies.CanUseLightningNodeInStore);
|
||||
|
||||
PolicyHasChild(Policies.CanManageUsers, Policies.CanCreateUser);
|
||||
PolicyHasChild(Policies.CanManagePullPayments, Policies.CanCreatePullPayments);
|
||||
PolicyHasChild(Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments);
|
||||
PolicyHasChild(Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests);
|
||||
PolicyHasChild(Policies.CanModifyProfile, Policies.CanViewProfile);
|
||||
PolicyHasChild(Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore);
|
||||
PolicyHasChild(Policies.CanManageNotificationsForUser, Policies.CanViewNotificationsForUser);
|
||||
PolicyHasChild(Policies.CanModifyServerSettings,
|
||||
PolicyHasChild(policyMap,Policies.CanManageUsers, Policies.CanCreateUser);
|
||||
PolicyHasChild(policyMap,Policies.CanManagePullPayments, Policies.CanCreatePullPayments);
|
||||
PolicyHasChild(policyMap,Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments);
|
||||
PolicyHasChild(policyMap,Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests);
|
||||
PolicyHasChild(policyMap,Policies.CanModifyProfile, Policies.CanViewProfile);
|
||||
PolicyHasChild(policyMap,Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore);
|
||||
PolicyHasChild(policyMap,Policies.CanManageNotificationsForUser, Policies.CanViewNotificationsForUser);
|
||||
PolicyHasChild(policyMap,Policies.CanModifyServerSettings,
|
||||
Policies.CanUseInternalLightningNode,
|
||||
Policies.CanManageUsers);
|
||||
PolicyHasChild(Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode);
|
||||
PolicyHasChild(Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts);
|
||||
PolicyHasChild(Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore);
|
||||
PolicyHasChild(Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests);
|
||||
PolicyHasChild(policyMap, Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode);
|
||||
PolicyHasChild(policyMap, Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts);
|
||||
PolicyHasChild(policyMap, Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore);
|
||||
PolicyHasChild(policyMap, Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests);
|
||||
|
||||
var missingPolicies = Policies.AllPolicies.ToHashSet();
|
||||
//recurse through the tree to see which policies are not included in the tree
|
||||
foreach (var policy in policyMap)
|
||||
{
|
||||
missingPolicies.Remove(policy.Key);
|
||||
foreach (var subPolicy in policy.Value)
|
||||
{
|
||||
missingPolicies.Remove(subPolicy);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var missingPolicy in missingPolicies)
|
||||
{
|
||||
policyMap.Add(missingPolicy, new HashSet<string>());
|
||||
}
|
||||
return new ReadOnlyDictionary<string, HashSet<string>>(policyMap);
|
||||
}
|
||||
|
||||
private static void PolicyHasChild(string policy, params string[] subPolicies)
|
||||
private static void PolicyHasChild(Dictionary<string, HashSet<string>>policyMap, string policy, params string[] subPolicies)
|
||||
{
|
||||
if (PolicyMap.TryGetValue(policy, out var existingSubPolicies))
|
||||
if (policyMap.TryGetValue(policy, out var existingSubPolicies))
|
||||
{
|
||||
foreach (string subPolicy in subPolicies)
|
||||
{
|
||||
@ -275,7 +296,7 @@ namespace BTCPayServer.Client
|
||||
}
|
||||
else
|
||||
{
|
||||
PolicyMap.Add(policy, subPolicies.ToHashSet());
|
||||
policyMap.Add(policy, subPolicies.ToHashSet());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.2.3" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.2.5" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(Altcoins)' != 'true'">
|
||||
|
@ -64,6 +64,7 @@ namespace BTCPayServer.Data
|
||||
public DbSet<U2FDevice> U2FDevices { get; set; }
|
||||
public DbSet<Fido2Credential> Fido2Credentials { get; set; }
|
||||
public DbSet<UserStore> UserStore { get; set; }
|
||||
public DbSet<StoreRole> StoreRoles { get; set; }
|
||||
[Obsolete]
|
||||
public DbSet<WalletData> Wallets { get; set; }
|
||||
public DbSet<WalletObjectData> WalletObjects { get; set; }
|
||||
@ -129,6 +130,7 @@ namespace BTCPayServer.Data
|
||||
PayoutProcessorData.OnModelCreating(builder, Database);
|
||||
WebhookData.OnModelCreating(builder, Database);
|
||||
FormData.OnModelCreating(builder, Database);
|
||||
StoreRole.OnModelCreating(builder, Database);
|
||||
|
||||
|
||||
if (Database.IsSqlite() && !_designTime)
|
||||
|
@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Data;
|
||||
@ -41,4 +43,7 @@ public class LightningAddressDataBlob
|
||||
public decimal? Max { get; set; }
|
||||
|
||||
public JObject InvoiceMetadata { get; set; }
|
||||
|
||||
[JsonExtensionData] public Dictionary<string, JToken> AdditionalData { get; set; }
|
||||
|
||||
}
|
||||
|
@ -1,13 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using PayoutProcessorData = BTCPayServer.Data.PayoutProcessorData;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@ -37,8 +34,6 @@ namespace BTCPayServer.Data
|
||||
|
||||
public byte[] StoreCertificate { get; set; }
|
||||
|
||||
[NotMapped] public string Role { get; set; }
|
||||
|
||||
public string StoreBlob { get; set; }
|
||||
|
||||
[Obsolete("Use GetDefaultPaymentId instead")]
|
||||
@ -52,6 +47,7 @@ namespace BTCPayServer.Data
|
||||
public IEnumerable<CustodianAccountData> CustodianAccounts { get; set; }
|
||||
public IEnumerable<StoreSettingData> Settings { get; set; }
|
||||
public IEnumerable<FormData> Forms { get; set; }
|
||||
public IEnumerable<StoreRole> StoreRoles { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
|
50
BTCPayServer.Data/Data/StoreRole.cs
Normal file
50
BTCPayServer.Data/Data/StoreRole.cs
Normal file
@ -0,0 +1,50 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Data;
|
||||
|
||||
public class StoreRole
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string StoreDataId { get; set; }
|
||||
public string Role { get; set; }
|
||||
public List<string> Permissions { get; set; }
|
||||
public List<UserStore> Users { get; set; }
|
||||
public StoreData StoreData { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
builder.Entity<StoreRole>(entity =>
|
||||
{
|
||||
entity.HasOne(e => e.StoreData)
|
||||
.WithMany(s => s.StoreRoles)
|
||||
.HasForeignKey(e => e.StoreDataId)
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired(false);
|
||||
|
||||
entity.HasIndex(entity => new {entity.StoreDataId, entity.Role}).IsUnique();
|
||||
});
|
||||
|
||||
|
||||
|
||||
if (!databaseFacade.IsNpgsql())
|
||||
{
|
||||
builder.Entity<StoreRole>()
|
||||
.Property(o => o.Permissions)
|
||||
.HasConversion(
|
||||
v => JsonConvert.SerializeObject(v),
|
||||
v => JsonConvert.DeserializeObject<List<string>>(v)?? new List<string>(),
|
||||
new ValueComparer<List<string>>(
|
||||
(c1, c2) => c1 ==c2 || c1 != null && c2 != null && c1.SequenceEqual(c2),
|
||||
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
|
||||
c => c.ToList()));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
@ -9,7 +10,10 @@ namespace BTCPayServer.Data
|
||||
|
||||
public string StoreDataId { get; set; }
|
||||
public StoreData StoreData { get; set; }
|
||||
public string Role { get; set; }
|
||||
[Column("Role")]
|
||||
public string StoreRoleId { get; set; }
|
||||
public StoreRole StoreRole { get; set; }
|
||||
|
||||
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder)
|
||||
@ -32,6 +36,10 @@ namespace BTCPayServer.Data
|
||||
.HasOne(pt => pt.StoreData)
|
||||
.WithMany(t => t.UserStores)
|
||||
.HasForeignKey(pt => pt.StoreDataId);
|
||||
|
||||
builder.Entity<UserStore>().HasOne(e => e.StoreRole)
|
||||
.WithMany(role => role.Users)
|
||||
.HasForeignKey(e => e.StoreRoleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class WebhookDeliveryData : IHasBlobUntyped
|
||||
public class WebhookDeliveryData
|
||||
{
|
||||
[Key]
|
||||
[MaxLength(25)]
|
||||
@ -17,10 +17,8 @@ namespace BTCPayServer.Data
|
||||
|
||||
[Required]
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
[Obsolete("Use Blob2 instead")]
|
||||
public byte[] Blob { get; set; }
|
||||
public string Blob2 { get; set; }
|
||||
|
||||
public string Blob { get; set; }
|
||||
public bool Pruned { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
@ -28,11 +26,11 @@ namespace BTCPayServer.Data
|
||||
.HasOne(o => o.Webhook)
|
||||
.WithMany(a => a.Deliveries).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<WebhookDeliveryData>().HasIndex(o => o.WebhookId);
|
||||
|
||||
builder.Entity<WebhookDeliveryData>().HasIndex(o => o.Timestamp);
|
||||
if (databaseFacade.IsNpgsql())
|
||||
{
|
||||
builder.Entity<WebhookDeliveryData>()
|
||||
.Property(o => o.Blob2)
|
||||
.Property(o => o.Blob)
|
||||
.HasColumnType("JSONB");
|
||||
}
|
||||
}
|
||||
|
106
BTCPayServer.Data/Migrations/20230504125505_StoreRoles.cs
Normal file
106
BTCPayServer.Data/Migrations/20230504125505_StoreRoles.cs
Normal file
@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20230504125505_StoreRoles")]
|
||||
public partial class StoreRoles : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
var permissionsType = migrationBuilder.IsNpgsql() ? "TEXT[]" : "TEXT";
|
||||
migrationBuilder.CreateTable(
|
||||
name: "StoreRoles",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
StoreDataId = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Role = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Permissions = table.Column<string>(type: permissionsType, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_StoreRoles", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_StoreRoles_Stores_StoreDataId",
|
||||
column: x => x.StoreDataId,
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StoreRoles_StoreDataId_Role",
|
||||
table: "StoreRoles",
|
||||
columns: new[] { "StoreDataId", "Role" },
|
||||
unique: true);
|
||||
|
||||
object GetPermissionsData(string[] permissions)
|
||||
{
|
||||
if (migrationBuilder.IsNpgsql())
|
||||
return permissions;
|
||||
return JsonConvert.SerializeObject(permissions);
|
||||
}
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
"StoreRoles",
|
||||
columns: new[] { "Id", "Role", "Permissions" },
|
||||
columnTypes: new[] { "TEXT", "TEXT", permissionsType },
|
||||
values: new object[,]
|
||||
{
|
||||
{
|
||||
"Owner", "Owner", GetPermissionsData(new[]
|
||||
{
|
||||
"btcpay.store.canmodifystoresettings",
|
||||
"btcpay.store.cantradecustodianaccount",
|
||||
"btcpay.store.canwithdrawfromcustodianaccount",
|
||||
"btcpay.store.candeposittocustodianaccount"
|
||||
})
|
||||
},
|
||||
{
|
||||
"Guest", "Guest", GetPermissionsData(new[]
|
||||
{
|
||||
"btcpay.store.canviewstoresettings",
|
||||
"btcpay.store.canmodifyinvoices",
|
||||
"btcpay.store.canviewcustodianaccounts",
|
||||
"btcpay.store.candeposittocustodianaccount"
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
if (this.SupportAddForeignKey(migrationBuilder.ActiveProvider))
|
||||
{
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_UserStore_StoreRoles_Role",
|
||||
table: "UserStore",
|
||||
column: "Role",
|
||||
principalTable: "StoreRoles",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
if (this.SupportDropForeignKey(migrationBuilder.ActiveProvider))
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_UserStore_StoreRoles_Role",
|
||||
table: "UserStore");
|
||||
}
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "StoreRoles");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20230529135505_WebhookDeliveriesCleanup")]
|
||||
public partial class WebhookDeliveriesCleanup : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (migrationBuilder.IsNpgsql())
|
||||
{
|
||||
migrationBuilder.Sql("DROP TABLE IF EXISTS \"InvoiceWebhookDeliveries\", \"WebhookDeliveries\";");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "WebhookDeliveries",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
WebhookId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Timestamp = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
Pruned = table.Column<bool>(type: "BOOLEAN", nullable: false),
|
||||
Blob = table.Column<string>(type: "JSONB", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_WebhookDeliveries", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_WebhookDeliveries_Webhooks_WebhookId",
|
||||
column: x => x.WebhookId,
|
||||
principalTable: "Webhooks",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_WebhookDeliveries_WebhookId",
|
||||
table: "WebhookDeliveries",
|
||||
column: "WebhookId");
|
||||
migrationBuilder.Sql("CREATE INDEX \"IX_WebhookDeliveries_Timestamp\" ON \"WebhookDeliveries\"(\"Timestamp\") WHERE \"Pruned\" IS FALSE");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "InvoiceWebhookDeliveries",
|
||||
columns: table => new
|
||||
{
|
||||
InvoiceId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
DeliveryId = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_InvoiceWebhookDeliveries", x => new { x.InvoiceId, x.DeliveryId });
|
||||
table.ForeignKey(
|
||||
name: "FK_InvoiceWebhookDeliveries_WebhookDeliveries_DeliveryId",
|
||||
column: x => x.DeliveryId,
|
||||
principalTable: "WebhookDeliveries",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_InvoiceWebhookDeliveries_Invoices_InvoiceId",
|
||||
column: x => x.InvoiceId,
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -214,56 +214,6 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("CustodianAccount");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Config")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Public")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.ToTable("Forms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<string>("PaymentMethod")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Processor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.ToTable("PayoutProcessors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@ -292,6 +242,31 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("Fido2Credentials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Config")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Public")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.ToTable("Forms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@ -655,6 +630,34 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("Payouts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<string>("Blob2")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PaymentMethod")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Processor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.ToTable("PayoutProcessors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@ -802,6 +805,28 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Permissions")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StoreDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreDataId", "Role")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("StoreRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreSettingData", b =>
|
||||
{
|
||||
b.Property<string>("StoreId")
|
||||
@ -878,13 +903,16 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("StoreDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.HasColumnType("TEXT");
|
||||
b.Property<string>("StoreRoleId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("Role");
|
||||
|
||||
b.HasKey("ApplicationUserId", "StoreDataId");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.HasIndex("StoreRoleId");
|
||||
|
||||
b.ToTable("UserStore");
|
||||
});
|
||||
|
||||
@ -991,12 +1019,12 @@ namespace BTCPayServer.Migrations
|
||||
.HasMaxLength(25)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<string>("Blob2")
|
||||
b.Property<string>("Blob")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Pruned")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("Timestamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@ -1007,6 +1035,8 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Timestamp");
|
||||
|
||||
b.HasIndex("WebhookId");
|
||||
|
||||
b.ToTable("WebhookDeliveries");
|
||||
@ -1188,26 +1218,6 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("StoreData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
||||
.WithMany("Forms")
|
||||
.HasForeignKey("StoreId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("Store");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
||||
.WithMany("PayoutProcessors")
|
||||
.HasForeignKey("StoreId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("Store");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
|
||||
@ -1218,6 +1228,16 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("ApplicationUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
||||
.WithMany("Forms")
|
||||
.HasForeignKey("StoreId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("Store");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
@ -1343,6 +1363,16 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("StoreData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
||||
.WithMany("PayoutProcessors")
|
||||
.HasForeignKey("StoreId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("Store");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
@ -1392,6 +1422,16 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("ApplicationUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("StoreRoles")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("StoreData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreSettingData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
||||
@ -1446,9 +1486,15 @@ namespace BTCPayServer.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("BTCPayServer.Data.StoreRole", "StoreRole")
|
||||
.WithMany("Users")
|
||||
.HasForeignKey("StoreRoleId");
|
||||
|
||||
b.Navigation("ApplicationUser");
|
||||
|
||||
b.Navigation("StoreData");
|
||||
|
||||
b.Navigation("StoreRole");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b =>
|
||||
@ -1606,9 +1652,16 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
b.Navigation("Settings");
|
||||
|
||||
b.Navigation("StoreRoles");
|
||||
|
||||
b.Navigation("UserStores");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
|
||||
{
|
||||
b.Navigation("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletData", b =>
|
||||
{
|
||||
b.Navigation("WalletTransactions");
|
||||
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
@ -245,7 +246,7 @@ namespace BTCPayServer.Tests
|
||||
await tester.EnsureChannelsSetup();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess(true);
|
||||
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
|
||||
user.RegisterLightningNode("BTC");
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
user.RegisterDerivationScheme("LTC");
|
||||
|
||||
@ -651,6 +652,7 @@ donation:
|
||||
price: 1.02
|
||||
custom: true
|
||||
";
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
Assert.Equal("hello", vmpos.Title);
|
||||
@ -662,7 +664,6 @@ donation:
|
||||
Assert.Equal("good apple", vmview.Items[0].Title);
|
||||
Assert.Equal("orange", vmview.Items[1].Title);
|
||||
Assert.Equal(10.0m, vmview.Items[1].Price.Value);
|
||||
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
|
||||
Assert.Equal("{0} Purchase", vmview.ButtonText);
|
||||
Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText);
|
||||
Assert.Equal("Wanna tip?", vmview.CustomTipText);
|
||||
@ -723,6 +724,7 @@ donation:
|
||||
price: 1.02
|
||||
custom: true
|
||||
";
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
publicApps = user.GetController<UIPointOfSaleController>();
|
||||
vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
|
||||
@ -750,6 +752,8 @@ inventoryitem:
|
||||
inventory: 1
|
||||
noninventoryitem:
|
||||
price: 10.0";
|
||||
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
|
||||
//inventoryitem has 1 item available
|
||||
@ -780,15 +784,13 @@ noninventoryitem:
|
||||
|
||||
//let's mark the inventoryitem invoice as invalid, this should return the item to back in stock
|
||||
var controller = tester.PayTester.GetController<UIInvoiceController>(user.UserId, user.StoreId);
|
||||
var appService = tester.PayTester.GetService<AppService>();
|
||||
var eventAggregator = tester.PayTester.GetService<EventAggregator>();
|
||||
Assert.IsType<JsonResult>(await controller.ChangeInvoiceState(inventoryItemInvoice.Id, "invalid"));
|
||||
//check that item is back in stock
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
Assert.Equal(1,
|
||||
appService.Parse(vmpos.Template, "BTC").Single(item => item.Id == "inventoryitem").Inventory);
|
||||
AppService.Parse(vmpos.Template).Single(item => item.Id == "inventoryitem").Inventory);
|
||||
}, 10000);
|
||||
|
||||
//test payment methods option
|
||||
@ -803,6 +805,8 @@ btconly:
|
||||
- BTC
|
||||
normal:
|
||||
price: 1.0";
|
||||
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "btconly").Result);
|
||||
@ -847,18 +851,19 @@ g:
|
||||
custom: topup
|
||||
";
|
||||
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
Assert.DoesNotContain("custom", vmpos.Template);
|
||||
var items = appService.Parse(vmpos.Template, vmpos.Currency);
|
||||
Assert.Contains(items, item => item.Id == "a" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "b" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "c" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum);
|
||||
Assert.Contains(items, item => item.Id == "d" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "e" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum);
|
||||
Assert.Contains(items, item => item.Id == "f" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup);
|
||||
Assert.Contains(items, item => item.Id == "g" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup);
|
||||
|
||||
var items = AppService.Parse(vmpos.Template);
|
||||
Assert.Contains(items, item => item.Id == "a" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "b" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "c" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum);
|
||||
Assert.Contains(items, item => item.Id == "d" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "e" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum);
|
||||
Assert.Contains(items, item => item.Id == "f" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup);
|
||||
Assert.Contains(items, item => item.Id == "g" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup);
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Static, null, null, null, null, null, "g").Result);
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
|
@ -1,13 +1,9 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Views.Stores;
|
||||
using NBitcoin;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Support.Extensions;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
@ -40,8 +36,10 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Configure store url
|
||||
var storeUrl = "https://satoshisteaks.com/";
|
||||
var supportUrl = "https://support.satoshisteaks.com/{InvoiceId}/";
|
||||
s.GoToStore();
|
||||
s.Driver.FindElement(By.Id("StoreWebsite")).SendKeys(storeUrl);
|
||||
s.Driver.FindElement(By.Id("StoreSupportUrl")).SendKeys(supportUrl);
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
|
||||
@ -64,9 +62,9 @@ namespace BTCPayServer.Tests
|
||||
var qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||
var address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
var payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||
var copyAddress = s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value");
|
||||
var copyAddress = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
|
||||
Assert.Equal($"bitcoin:{address}", payUrl);
|
||||
Assert.StartsWith("bcrt", s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value"));
|
||||
Assert.StartsWith("bcrt", s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text);
|
||||
Assert.DoesNotContain("lightning=", payUrl);
|
||||
Assert.Equal(address, copyAddress);
|
||||
Assert.Equal($"bitcoin:{address.ToUpperInvariant()}", qrValue);
|
||||
@ -86,7 +84,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||
Assert.StartsWith("lightning:lnurl", payUrl);
|
||||
Assert.StartsWith("lnurl", s.Driver.WaitForElement(By.Id("Lightning_BTC")).GetAttribute("value"));
|
||||
Assert.StartsWith("lnurl", s.Driver.WaitForElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text);
|
||||
s.Driver.ElementDoesNotExist(By.Id("Address_BTC"));
|
||||
});
|
||||
|
||||
@ -101,7 +99,7 @@ namespace BTCPayServer.Tests
|
||||
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||
copyAddress = s.Driver.FindElement(By.Id("Lightning_BTC_LightningLike")).GetAttribute("value");
|
||||
copyAddress = s.Driver.FindElement(By.CssSelector("#Lightning_BTC_LightningLike .truncate-center-start")).Text;
|
||||
Assert.Equal($"lightning:{address}", payUrl);
|
||||
Assert.Equal(address, copyAddress);
|
||||
Assert.Equal($"lightning:{address.ToUpperInvariant()}", qrValue);
|
||||
@ -140,8 +138,47 @@ namespace BTCPayServer.Tests
|
||||
var expiredSection = s.Driver.FindElement(By.Id("unpaid"));
|
||||
Assert.True(expiredSection.Displayed);
|
||||
Assert.Contains("Invoice Expired", expiredSection.Text);
|
||||
Assert.Contains("resubmit a payment", expiredSection.Text);
|
||||
Assert.DoesNotContain("This invoice expired with partial payment", expiredSection.Text);
|
||||
|
||||
});
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("receipt-btn")));
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ContactLink")));
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ReceiptLink")));
|
||||
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
|
||||
|
||||
// Expire paid partial
|
||||
s.GoToHome();
|
||||
invoiceId = s.CreateInvoice(2100, "EUR");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
|
||||
await Task.Delay(200);
|
||||
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
var amountFraction = "0.00001";
|
||||
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
|
||||
Money.Parse(amountFraction));
|
||||
await s.Server.ExplorerNode.GenerateAsync(1);
|
||||
|
||||
expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
|
||||
expirySeconds.Clear();
|
||||
expirySeconds.SendKeys("3");
|
||||
s.Driver.FindElement(By.Id("Expire")).Click();
|
||||
|
||||
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
|
||||
Assert.Contains("The invoice hasn't been paid in full.", paymentInfo.Text);
|
||||
Assert.Contains("Please send", paymentInfo.Text);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var expiredSection = s.Driver.FindElement(By.Id("unpaid"));
|
||||
Assert.True(expiredSection.Displayed);
|
||||
Assert.Contains("Invoice Expired", expiredSection.Text);
|
||||
Assert.Contains("This invoice expired with partial payment", expiredSection.Text);
|
||||
Assert.DoesNotContain("resubmit a payment", expiredSection.Text);
|
||||
});
|
||||
var contactLink = s.Driver.FindElement(By.Id("ContactLink"));
|
||||
Assert.Equal("Contact us", contactLink.Text);
|
||||
Assert.Matches(supportUrl.Replace("{InvoiceId}", invoiceId), contactLink.GetAttribute("href"));
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ReceiptLink")));
|
||||
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
|
||||
|
||||
// Test payment
|
||||
@ -166,7 +203,7 @@ namespace BTCPayServer.Tests
|
||||
// Pay partial amount
|
||||
await Task.Delay(200);
|
||||
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
var amountFraction = "0.00001";
|
||||
amountFraction = "0.00001";
|
||||
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
|
||||
Money.Parse(amountFraction));
|
||||
await s.Server.ExplorerNode.GenerateAsync(1);
|
||||
@ -210,7 +247,8 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("Invoice Paid", settledSection.Text);
|
||||
});
|
||||
s.Driver.FindElement(By.Id("confetti"));
|
||||
s.Driver.FindElement(By.Id("receipt-btn"));
|
||||
s.Driver.FindElement(By.Id("ReceiptLink"));
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ContactLink")));
|
||||
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
|
||||
|
||||
// BIP21
|
||||
@ -229,8 +267,8 @@ namespace BTCPayServer.Tests
|
||||
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||
var copyAddressOnchain = s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value");
|
||||
var copyAddressLightning = s.Driver.FindElement(By.Id("Lightning_BTC")).GetAttribute("value");
|
||||
var copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
|
||||
var copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text;
|
||||
Assert.StartsWith($"bitcoin:{address}?amount=", payUrl);
|
||||
Assert.Contains("?amount=", payUrl);
|
||||
Assert.Contains("&lightning=", payUrl);
|
||||
@ -297,8 +335,8 @@ namespace BTCPayServer.Tests
|
||||
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||
copyAddressOnchain = s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value");
|
||||
copyAddressLightning = s.Driver.FindElement(By.Id("Lightning_BTC")).GetAttribute("value");
|
||||
copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
|
||||
copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text;
|
||||
Assert.StartsWith($"bitcoin:{address}", payUrl);
|
||||
Assert.Contains("?lightning=lnurl", payUrl);
|
||||
Assert.DoesNotContain("amount=", payUrl);
|
||||
@ -358,6 +396,7 @@ namespace BTCPayServer.Tests
|
||||
s.GoToHome();
|
||||
s.GoToLightningSettings();
|
||||
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), false);
|
||||
s.Driver.ScrollTo(By.Id("save"));
|
||||
s.Driver.FindElement(By.Id("save")).Click();
|
||||
Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text);
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
@ -55,7 +56,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Empty(appList2.Apps);
|
||||
Assert.Equal("test", appList.Apps[0].AppName);
|
||||
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
|
||||
Assert.True(appList.Apps[0].IsOwner);
|
||||
Assert.True(appList.Apps[0].Role.ToPermissionSet(appList.Apps[0].StoreId).Contains(Policies.CanModifyStoreSettings, appList.Apps[0].StoreId));
|
||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
|
||||
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
|
||||
|
@ -26,6 +26,7 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@ -1042,14 +1043,13 @@ namespace BTCPayServer.Tests
|
||||
[Fact]
|
||||
public void CanParseFilter()
|
||||
{
|
||||
var storeId = "6DehZnc9S7qC6TUTNWuzJ1pFsHTHvES6An21r3MjvLey";
|
||||
var filter = "storeid:abc, status:abed, blabhbalh ";
|
||||
var search = new SearchString(filter);
|
||||
Assert.Equal("storeid:abc, status:abed, blabhbalh", search.ToString());
|
||||
Assert.Equal("blabhbalh", search.TextSearch);
|
||||
Assert.Single(search.Filters["storeid"]);
|
||||
Assert.Single(search.Filters["status"]);
|
||||
Assert.Equal("abc", search.Filters["storeid"].First());
|
||||
Assert.Equal("abed", search.Filters["status"].First());
|
||||
Assert.Single(search.Filters["storeid"], "abc");
|
||||
Assert.Single(search.Filters["status"], "abed");
|
||||
|
||||
filter = "status:abed, status:abed2";
|
||||
search = new SearchString(filter);
|
||||
@ -1064,6 +1064,48 @@ namespace BTCPayServer.Tests
|
||||
search = new SearchString(filter);
|
||||
Assert.Equal("2019-04-25 01:00 AM", search.Filters["startdate"].First());
|
||||
Assert.Equal("hekki", search.TextSearch);
|
||||
|
||||
// modify search
|
||||
filter = $"status:settled,exceptionstatus:paidLate,unusual:true, fulltext searchterm, storeid:{storeId},startdate:2019-04-25 01:00:00";
|
||||
search = new SearchString(filter);
|
||||
Assert.Equal(filter, search.ToString());
|
||||
Assert.Equal("fulltext searchterm", search.TextSearch);
|
||||
Assert.Single(search.Filters["storeid"], storeId);
|
||||
Assert.Single(search.Filters["status"], "settled");
|
||||
Assert.Single(search.Filters["exceptionstatus"], "paidLate");
|
||||
Assert.Single(search.Filters["unusual"], "true");
|
||||
|
||||
// toggle off bool with same value
|
||||
var modified = new SearchString(search.Toggle("unusual", "true"));
|
||||
Assert.Null(modified.GetFilterBool("unusual"));
|
||||
|
||||
// add to array
|
||||
modified = new SearchString(modified.Toggle("status", "processing"));
|
||||
var statusArray = modified.GetFilterArray("status");
|
||||
Assert.Equal(2, statusArray.Length);
|
||||
Assert.Contains("processing", statusArray);
|
||||
Assert.Contains("settled", statusArray);
|
||||
|
||||
// toggle off array with same value
|
||||
modified = new SearchString(modified.Toggle("status", "settled"));
|
||||
statusArray = modified.GetFilterArray("status");
|
||||
Assert.Single(statusArray, "processing");
|
||||
|
||||
// toggle off array with null value
|
||||
modified = new SearchString(modified.Toggle("status", null));
|
||||
Assert.Null(modified.GetFilterArray("status"));
|
||||
|
||||
// toggle off date with null value
|
||||
modified = new SearchString(modified.Toggle("startdate", "-7d"));
|
||||
Assert.Single(modified.GetFilterArray("startdate"), "-7d");
|
||||
modified = new SearchString(modified.Toggle("startdate", null));
|
||||
Assert.Null(modified.GetFilterArray("startdate"));
|
||||
|
||||
// toggle off date with same value
|
||||
modified = new SearchString(modified.Toggle("enddate", "-7d"));
|
||||
Assert.Single(modified.GetFilterArray("enddate"), "-7d");
|
||||
modified = new SearchString(modified.Toggle("enddate", "-7d"));
|
||||
Assert.Null(modified.GetFilterArray("enddate"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -1341,6 +1383,24 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(cache.States[0].Rates[0].Pair, cache2.States[0].Rates[0].Pair);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseStoreRoleId()
|
||||
{
|
||||
var id = StoreRoleId.Parse("test::lol");
|
||||
Assert.Equal("test", id.StoreId);
|
||||
Assert.Equal("lol", id.Role);
|
||||
Assert.Equal("test::lol", id.ToString());
|
||||
Assert.Equal("test::lol", id.Id);
|
||||
Assert.False(id.IsServerRole);
|
||||
|
||||
id = StoreRoleId.Parse("lol");
|
||||
Assert.Null(id.StoreId);
|
||||
Assert.Equal("lol", id.Role);
|
||||
Assert.Equal("lol", id.ToString());
|
||||
Assert.Equal("lol", id.Id);
|
||||
Assert.True(id.IsServerRole);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KitchenSinkTest()
|
||||
{
|
||||
|
@ -15,12 +15,16 @@ using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.PayoutProcessors.OnChain;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Custodian.Client.MockCustodian;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
@ -225,7 +229,7 @@ namespace BTCPayServer.Tests
|
||||
await Assert.ThrowsAsync<GreenfieldAPIException>(() => newUserClient.GetInvoices(store.Id));
|
||||
|
||||
// if user is a guest or owner, then it should be ok
|
||||
await unrestricted.AddStoreUser(store.Id, new StoreUserData() { UserId = newUser.Id, Role = "Guest" });
|
||||
await unrestricted.AddStoreUser(store.Id, new StoreUserData() { UserId = newUser.Id});
|
||||
await newUserClient.GetInvoices(store.Id);
|
||||
}
|
||||
|
||||
@ -1316,7 +1320,8 @@ namespace BTCPayServer.Tests
|
||||
// We strip the user's Owner right, so the key should not work
|
||||
using var ctx = tester.PayTester.GetService<Data.ApplicationDbContextFactory>().CreateContext();
|
||||
var storeEntity = await ctx.UserStore.SingleAsync(u => u.ApplicationUserId == user.UserId && u.StoreDataId == newStore.Id);
|
||||
storeEntity.Role = "Guest";
|
||||
var roleId = (await tester.PayTester.GetService<StoreRepository>().GetStoreRoles(null)).Single(r => r.Role == "Guest").Id;
|
||||
storeEntity.StoreRoleId = roleId;
|
||||
await ctx.SaveChangesAsync();
|
||||
await AssertHttpError(403, async () => await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" }));
|
||||
|
||||
@ -1427,7 +1432,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.False(hook.AutomaticRedelivery);
|
||||
Assert.Equal(fakeServer.ServerUri.AbsoluteUri, hook.Url);
|
||||
}
|
||||
using var tester = CreateServerTester();
|
||||
using var tester = CreateServerTester(newDb: true);
|
||||
using var fakeServer = new FakeServer();
|
||||
await fakeServer.Start();
|
||||
await tester.StartAsync();
|
||||
@ -1504,6 +1509,14 @@ namespace BTCPayServer.Tests
|
||||
clientProfile = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanCreateInvoice);
|
||||
await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId);
|
||||
|
||||
|
||||
TestLogs.LogInformation("Can prune deliveries");
|
||||
var cleanup = tester.PayTester.GetService<HostedServices.CleanupWebhookDeliveriesTask>();
|
||||
cleanup.BatchSize = 1;
|
||||
cleanup.PruneAfter = TimeSpan.Zero;
|
||||
await cleanup.Do(default);
|
||||
await AssertHttpError(409, () => clientProfile.RedeliverWebhook(user.StoreId, hook.Id, delivery.Id));
|
||||
|
||||
TestLogs.LogInformation("Testing corner cases");
|
||||
Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, "lol", newDeliveryId));
|
||||
Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, "lol"));
|
||||
@ -1947,6 +1960,82 @@ namespace BTCPayServer.Tests
|
||||
CustomCurrency = "BTC"
|
||||
});
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
|
||||
// test subtract percentage
|
||||
validationError = await AssertValidationError(new[] { "SubtractPercentage" }, async () =>
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.RateThen,
|
||||
SubtractPercentage = 101
|
||||
});
|
||||
});
|
||||
Assert.Contains("SubtractPercentage: Percentage must be a numeric value between 0 and 100", validationError.Message);
|
||||
|
||||
// should auto-approve
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.RateThen,
|
||||
SubtractPercentage = 6.15m
|
||||
});
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
Assert.Equal(0.9385m, pp.Amount);
|
||||
|
||||
// test RefundVariant.OverpaidAmount
|
||||
validationError = await AssertValidationError(new[] { "RefundVariant" }, async () =>
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.OverpaidAmount
|
||||
});
|
||||
});
|
||||
Assert.Contains("Invoice is not overpaid", validationError.Message);
|
||||
|
||||
// should auto-approve
|
||||
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest { Amount = 5000.0m, Currency = "USD" });
|
||||
methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id);
|
||||
method = methods.First();
|
||||
|
||||
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
|
||||
{
|
||||
await tester.ExplorerNode.SendToAddressAsync(
|
||||
BitcoinAddress.Create(method.Destination, tester.NetworkProvider.BTC.NBitcoinNetwork),
|
||||
Money.Coins(method.Due * 2)
|
||||
);
|
||||
});
|
||||
|
||||
await tester.ExplorerNode.GenerateAsync(5);
|
||||
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
invoice = await client.GetInvoice(user.StoreId, invoice.Id);
|
||||
Assert.True(invoice.Status == InvoiceStatus.Settled);
|
||||
Assert.True(invoice.AdditionalStatus == InvoiceExceptionStatus.PaidOver);
|
||||
});
|
||||
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.OverpaidAmount
|
||||
});
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
Assert.Equal(method.Due, pp.Amount);
|
||||
|
||||
// once more with subtract percentage
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.OverpaidAmount,
|
||||
SubtractPercentage = 21m
|
||||
});
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
Assert.Equal(0.79m, pp.Amount);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -2254,7 +2343,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Single(paymentMethods);
|
||||
Assert.True(paymentMethods.First().Activated);
|
||||
|
||||
var invoiceWithdefaultPaymentMethodLN = await client.CreateInvoice(user.StoreId,
|
||||
var invoiceWithDefaultPaymentMethodLN = await client.CreateInvoice(user.StoreId,
|
||||
new CreateInvoiceRequest()
|
||||
{
|
||||
Currency = "USD",
|
||||
@ -2265,9 +2354,9 @@ namespace BTCPayServer.Tests
|
||||
DefaultPaymentMethod = "BTC_LightningLike"
|
||||
}
|
||||
});
|
||||
Assert.Equal("BTC_LightningLike", invoiceWithdefaultPaymentMethodLN.Checkout.DefaultPaymentMethod);
|
||||
Assert.Equal("BTC_LightningLike", invoiceWithDefaultPaymentMethodLN.Checkout.DefaultPaymentMethod);
|
||||
|
||||
var invoiceWithdefaultPaymentMethodOnChain = await client.CreateInvoice(user.StoreId,
|
||||
var invoiceWithDefaultPaymentMethodOnChain = await client.CreateInvoice(user.StoreId,
|
||||
new CreateInvoiceRequest()
|
||||
{
|
||||
Currency = "USD",
|
||||
@ -2278,13 +2367,35 @@ namespace BTCPayServer.Tests
|
||||
DefaultPaymentMethod = "BTC"
|
||||
}
|
||||
});
|
||||
Assert.Equal("BTC", invoiceWithdefaultPaymentMethodOnChain.Checkout.DefaultPaymentMethod);
|
||||
|
||||
Assert.Equal("BTC", invoiceWithDefaultPaymentMethodOnChain.Checkout.DefaultPaymentMethod);
|
||||
|
||||
// reset lazy payment methods
|
||||
store = await client.GetStore(user.StoreId);
|
||||
store.LazyPaymentMethods = false;
|
||||
store = await client.UpdateStore(store.Id,
|
||||
JObject.FromObject(store).ToObject<UpdateStoreRequest>());
|
||||
Assert.False(store.LazyPaymentMethods);
|
||||
|
||||
// use store default payment method
|
||||
store = await client.GetStore(user.StoreId);
|
||||
Assert.Null(store.DefaultPaymentMethod);
|
||||
var storeDefaultPaymentMethod = "BTC-LightningNetwork";
|
||||
store.DefaultPaymentMethod = storeDefaultPaymentMethod;
|
||||
store = await client.UpdateStore(store.Id,
|
||||
JObject.FromObject(store).ToObject<UpdateStoreRequest>());
|
||||
Assert.Equal(storeDefaultPaymentMethod, store.DefaultPaymentMethod);
|
||||
|
||||
var invoiceWithStoreDefaultPaymentMethod = await client.CreateInvoice(user.StoreId,
|
||||
new CreateInvoiceRequest()
|
||||
{
|
||||
Currency = "USD",
|
||||
Amount = 100,
|
||||
Checkout = new CreateInvoiceRequest.CheckoutOptions()
|
||||
{
|
||||
PaymentMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
|
||||
}
|
||||
});
|
||||
Assert.Equal(storeDefaultPaymentMethod, invoiceWithStoreDefaultPaymentMethod.Checkout.DefaultPaymentMethod);
|
||||
|
||||
//let's see the overdue amount
|
||||
invoice = await client.CreateInvoice(user.StoreId,
|
||||
@ -2341,27 +2452,10 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotNull(merchantInvoice.PaymentHash);
|
||||
Assert.Equal(merchantInvoice.Id, merchantInvoice.PaymentHash);
|
||||
|
||||
// The default client is using charge, so we should not be able to query channels
|
||||
var chargeClient = await user.CreateClient(Policies.CanUseInternalLightningNode);
|
||||
|
||||
var info = await chargeClient.GetLightningNodeInfo("BTC");
|
||||
Assert.Single(info.NodeURIs);
|
||||
Assert.NotEqual(0, info.BlockHeight);
|
||||
Assert.NotNull(info.Alias);
|
||||
Assert.NotNull(info.Color);
|
||||
Assert.NotNull(info.Version);
|
||||
Assert.NotNull(info.PeersCount);
|
||||
Assert.NotNull(info.ActiveChannelsCount);
|
||||
Assert.NotNull(info.InactiveChannelsCount);
|
||||
Assert.NotNull(info.PendingChannelsCount);
|
||||
|
||||
var gex = await AssertAPIError("lightning-node-unavailable", () => chargeClient.ConnectToLightningNode("BTC", new ConnectToNodeRequest(NodeInfo.Parse($"{new Key().PubKey.ToHex()}@localhost:3827"))));
|
||||
Assert.Contains("NotSupported", gex.Message);
|
||||
|
||||
await AssertAPIError("lightning-node-unavailable", () => chargeClient.GetLightningNodeChannels("BTC"));
|
||||
var client = await user.CreateClient(Policies.CanUseInternalLightningNode);
|
||||
// Not permission for the store!
|
||||
await AssertAPIError("missing-permission", () => chargeClient.GetLightningNodeChannels(user.StoreId, "BTC"));
|
||||
var invoiceData = await chargeClient.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
|
||||
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels(user.StoreId, "BTC"));
|
||||
var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
|
||||
{
|
||||
Amount = LightMoney.Satoshis(1000),
|
||||
Description = "lol",
|
||||
@ -2369,17 +2463,17 @@ namespace BTCPayServer.Tests
|
||||
PrivateRouteHints = false
|
||||
});
|
||||
var chargeInvoice = invoiceData;
|
||||
Assert.NotNull(await chargeClient.GetLightningInvoice("BTC", invoiceData.Id));
|
||||
Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id));
|
||||
|
||||
// check list for internal node
|
||||
var invoices = await chargeClient.GetLightningInvoices("BTC");
|
||||
var pendingInvoices = await chargeClient.GetLightningInvoices("BTC", true);
|
||||
var invoices = await client.GetLightningInvoices("BTC");
|
||||
var pendingInvoices = await client.GetLightningInvoices("BTC", true);
|
||||
Assert.NotEmpty(invoices);
|
||||
Assert.Contains(invoices, i => i.Id == invoiceData.Id);
|
||||
Assert.NotEmpty(pendingInvoices);
|
||||
Assert.Contains(pendingInvoices, i => i.Id == invoiceData.Id);
|
||||
|
||||
var client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}");
|
||||
client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}");
|
||||
// Not permission for the server
|
||||
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels("BTC"));
|
||||
|
||||
@ -2458,7 +2552,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains(payments, i => i.BOLT11 == merchantInvoice.BOLT11);
|
||||
|
||||
// Node info
|
||||
info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
|
||||
var info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
|
||||
Assert.Single(info.NodeURIs);
|
||||
Assert.NotEqual(0, info.BlockHeight);
|
||||
|
||||
@ -2499,7 +2593,12 @@ namespace BTCPayServer.Tests
|
||||
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
|
||||
|
||||
var client = await user.CreateClient(Policies.Unrestricted);
|
||||
var invoice = await client.CreateInvoice(user.StoreId,
|
||||
var invoices = new Task<Client.Models.InvoiceData>[5];
|
||||
|
||||
// Create invoices
|
||||
for (int i = 0; i < invoices.Length; i++)
|
||||
{
|
||||
invoices[i] = client.CreateInvoice(user.StoreId,
|
||||
new CreateInvoiceRequest
|
||||
{
|
||||
Currency = "USD",
|
||||
@ -2510,18 +2609,35 @@ namespace BTCPayServer.Tests
|
||||
DefaultPaymentMethod = "BTC_LightningLike"
|
||||
}
|
||||
});
|
||||
var pm = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id));
|
||||
Assert.False(pm.AdditionalData.HasValues);
|
||||
}
|
||||
|
||||
var resp = await tester.CustomerLightningD.Pay(pm.Destination);
|
||||
Assert.Equal(PayResult.Ok, resp.Result);
|
||||
Assert.NotNull(resp.Details.PaymentHash);
|
||||
Assert.NotNull(resp.Details.Preimage);
|
||||
var pm = new InvoicePaymentMethodDataModel[invoices.Length];
|
||||
for (int i = 0; i < invoices.Length; i++)
|
||||
{
|
||||
pm[i] = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, (await invoices[i]).Id));
|
||||
Assert.False(pm[i].AdditionalData.HasValues);
|
||||
}
|
||||
|
||||
pm = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id));
|
||||
Assert.True(pm.AdditionalData.HasValues);
|
||||
Assert.Equal(resp.Details.PaymentHash.ToString(), pm.AdditionalData.GetValue("paymentHash"));
|
||||
Assert.Equal(resp.Details.Preimage.ToString(), pm.AdditionalData.GetValue("preimage"));
|
||||
// Pay them all at once
|
||||
Task<PayResponse>[] payResponses = new Task<PayResponse>[invoices.Length];
|
||||
for (int i = 0; i < invoices.Length; i++)
|
||||
{
|
||||
payResponses[i] = tester.CustomerLightningD.Pay(pm[i].Destination);
|
||||
}
|
||||
|
||||
// Checking the results
|
||||
for (int i = 0; i < invoices.Length; i++)
|
||||
{
|
||||
var resp = await payResponses[i];
|
||||
Assert.Equal(PayResult.Ok, resp.Result);
|
||||
Assert.NotNull(resp.Details.PaymentHash);
|
||||
Assert.NotNull(resp.Details.Preimage);
|
||||
|
||||
pm[i] = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, (await invoices[i]).Id));
|
||||
Assert.True(pm[i].AdditionalData.HasValues);
|
||||
Assert.Equal(resp.Details.PaymentHash.ToString(), pm[i].AdditionalData.GetValue("paymentHash"));
|
||||
Assert.Equal(resp.Details.Preimage.ToString(), pm[i].AdditionalData.GetValue("preimage"));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 20 * 1000)]
|
||||
@ -3259,11 +3375,16 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings);
|
||||
|
||||
var roles = await client.GetServerRoles();
|
||||
Assert.Equal(2,roles.Count);
|
||||
#pragma warning disable CS0618
|
||||
var ownerRole = roles.Single(data => data.Role == StoreRoles.Owner);
|
||||
var guestRole = roles.Single(data => data.Role == StoreRoles.Guest);
|
||||
#pragma warning restore CS0618
|
||||
var users = await client.GetStoreUsers(user.StoreId);
|
||||
var storeuser = Assert.Single(users);
|
||||
Assert.Equal(user.UserId, storeuser.UserId);
|
||||
Assert.Equal(StoreRoles.Owner, storeuser.Role);
|
||||
|
||||
Assert.Equal(ownerRole.Id, storeuser.Role);
|
||||
var user2 = tester.NewAccount();
|
||||
await user2.GrantAccessAsync(false);
|
||||
|
||||
@ -3274,7 +3395,7 @@ namespace BTCPayServer.Tests
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = StoreRoles.Guest, UserId = user2.UserId });
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = guestRole.Id, UserId = user2.UserId });
|
||||
|
||||
//test no access to api when only a guest
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.GetStoreUsers(user.StoreId));
|
||||
@ -3288,10 +3409,10 @@ namespace BTCPayServer.Tests
|
||||
await user2Client.GetStore(user.StoreId));
|
||||
|
||||
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = StoreRoles.Owner, UserId = user2.UserId });
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = ownerRole.Id, UserId = user2.UserId });
|
||||
await AssertAPIError("duplicate-store-user-role", async () =>
|
||||
await client.AddStoreUser(user.StoreId,
|
||||
new StoreUserData() { Role = StoreRoles.Owner, UserId = user2.UserId }));
|
||||
new StoreUserData() { Role = ownerRole.Id, UserId = user2.UserId }));
|
||||
await user2Client.RemoveStoreUser(user.StoreId, user.UserId);
|
||||
|
||||
|
||||
@ -3409,8 +3530,8 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
Assert.Empty(await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork"));
|
||||
await adminClient.UpdateStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork",
|
||||
new LightningAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(2) });
|
||||
Assert.Equal(2, Assert.Single(await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork")).IntervalSeconds.TotalSeconds);
|
||||
new LightningAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(600) });
|
||||
Assert.Equal(600, Assert.Single(await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork")).IntervalSeconds.TotalSeconds);
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var payoutC =
|
||||
@ -3495,8 +3616,8 @@ namespace BTCPayServer.Tests
|
||||
Assert.Empty(await adminClient.GetPayoutProcessors(admin.StoreId));
|
||||
|
||||
await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC",
|
||||
new OnChainAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(100000) });
|
||||
Assert.Equal(100000, Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds);
|
||||
new OnChainAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(3600) });
|
||||
Assert.Equal(3600, Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds);
|
||||
|
||||
var tpGen = Assert.Single(await adminClient.GetPayoutProcessors(admin.StoreId));
|
||||
Assert.Equal("BTC", Assert.Single(tpGen.PaymentMethods));
|
||||
@ -3509,8 +3630,10 @@ namespace BTCPayServer.Tests
|
||||
Assert.Empty(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
|
||||
Assert.Empty(await adminClient.GetPayoutProcessors(admin.StoreId));
|
||||
|
||||
// Send just enough money to cover the smallest of the payouts.
|
||||
var fee = (await tester.PayTester.GetService<IFeeProviderFactory>().CreateFeeProvider(tester.DefaultNetwork).GetFeeRateAsync(100)).GetFee(150);
|
||||
await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.000012m));
|
||||
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.00001m) + fee);
|
||||
await tester.ExplorerNode.GenerateAsync(1);
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
@ -3520,8 +3643,9 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(3, payouts.Length);
|
||||
});
|
||||
await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC",
|
||||
new OnChainAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(5) });
|
||||
Assert.Equal(5, Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds);
|
||||
new OnChainAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(600), FeeBlockTarget = 1000 });
|
||||
Assert.Equal(600, Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds);
|
||||
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
Assert.Equal(2, (await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")).Count());
|
||||
@ -3529,8 +3653,10 @@ namespace BTCPayServer.Tests
|
||||
Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress));
|
||||
});
|
||||
|
||||
await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m));
|
||||
var txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m) + fee);
|
||||
await tester.WaitForEvent<NewOnChainTransactionEvent>(null, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
|
||||
await tester.PayTester.GetService<PayoutProcessorService>().Restart(new PayoutProcessorService.PayoutProcessorQuery(admin.StoreId, "BTC"));
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
Assert.Equal(4, (await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")).Count());
|
||||
|
@ -1,10 +1,12 @@
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
@ -19,6 +21,74 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseOldYmlCorrectly()
|
||||
{
|
||||
var testOriginalDefaultYmlTemplate = @"
|
||||
green tea:
|
||||
price: 1
|
||||
title: Green Tea
|
||||
description: Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years.
|
||||
image: ~/img/pos-sample/green-tea.jpg
|
||||
|
||||
black tea:
|
||||
price: 1
|
||||
title: Black Tea
|
||||
description: Tian Jian Tian Jian means 'heavenly tippy tea' in Chinese, and it describes the finest grade of dark tea. Our Tian Jian dark tea is from Hunan province which is famous for making some of the best dark teas available.
|
||||
image: ~/img/pos-sample/black-tea.jpg
|
||||
|
||||
rooibos:
|
||||
price: 1.2
|
||||
title: Rooibos
|
||||
description: Rooibos is a dramatic red tea made from a South African herb that contains polyphenols and flavonoids. Often called 'African redbush tea', Rooibos herbal tea delights the senses and delivers potential health benefits with each caffeine-free sip.
|
||||
image: ~/img/pos-sample/rooibos.jpg
|
||||
|
||||
pu erh:
|
||||
price: 2
|
||||
title: Pu Erh
|
||||
description: This loose pur-erh tea is produced in Yunnan Province, China. The process in a relatively high humidity environment has mellowed the elemental character of the tea when compared to young Pu-erh.
|
||||
image: ~/img/pos-sample/pu-erh.jpg
|
||||
|
||||
herbal tea:
|
||||
price: 1.8
|
||||
title: Herbal Tea
|
||||
description: Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!
|
||||
image: ~/img/pos-sample/herbal-tea.jpg
|
||||
custom: true
|
||||
|
||||
fruit tea:
|
||||
price: 1.5
|
||||
title: Fruit Tea
|
||||
description: The Tibetan Himalayas, the land is majestic and beautiful—a spiritual place where, despite the perilous environment, many journey seeking enlightenment. Pay us what you want!
|
||||
image: ~/img/pos-sample/fruit-tea.jpg
|
||||
inventory: 5
|
||||
custom: true
|
||||
";
|
||||
var parsedDefault = MigrationStartupTask.ParsePOSYML(testOriginalDefaultYmlTemplate);
|
||||
Assert.Equal(6, parsedDefault.Length);
|
||||
Assert.Equal( "Green Tea" ,parsedDefault[0].Title);
|
||||
Assert.Equal( "green tea" ,parsedDefault[0].Id);
|
||||
Assert.Equal( "Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years." ,parsedDefault[0].Description);
|
||||
Assert.Null( parsedDefault[0].BuyButtonText);
|
||||
Assert.Equal( "~/img/pos-sample/green-tea.jpg" ,parsedDefault[0].Image);
|
||||
Assert.Equal( 1 ,parsedDefault[0].Price);
|
||||
Assert.Equal( ViewPointOfSaleViewModel.ItemPriceType.Fixed ,parsedDefault[0].PriceType);
|
||||
Assert.Null( parsedDefault[0].AdditionalData);
|
||||
Assert.Null( parsedDefault[0].PaymentMethods);
|
||||
|
||||
|
||||
Assert.Equal( "Herbal Tea" ,parsedDefault[4].Title);
|
||||
Assert.Equal( "herbal tea" ,parsedDefault[4].Id);
|
||||
Assert.Equal( "Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!" ,parsedDefault[4].Description);
|
||||
Assert.Null( parsedDefault[4].BuyButtonText);
|
||||
Assert.Equal( "~/img/pos-sample/herbal-tea.jpg" ,parsedDefault[4].Image);
|
||||
Assert.Equal( 1.8m ,parsedDefault[4].Price);
|
||||
Assert.Equal( ViewPointOfSaleViewModel.ItemPriceType.Minimum ,parsedDefault[4].PriceType);
|
||||
Assert.Null( parsedDefault[4].AdditionalData);
|
||||
Assert.Null( parsedDefault[4].PaymentMethods);
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUsePoSApp1()
|
||||
@ -53,6 +123,7 @@ donation:
|
||||
price: 1.02
|
||||
custom: true
|
||||
";
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
var publicApps = user.GetController<UIPointOfSaleController>();
|
||||
|
@ -180,7 +180,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Driver.FindElement(By.Id("StoreSelectorToggle")).Click();
|
||||
}
|
||||
Driver.WaitForElement(By.Id("StoreSelectorCreate")).Click();
|
||||
GoToUrl("/stores/create");
|
||||
var name = "Store" + RandomUtils.GetUInt64();
|
||||
TestLogs.LogInformation($"Created store {name}");
|
||||
Driver.WaitForElement(By.Id("Name")).SendKeys(name);
|
||||
@ -313,8 +313,6 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var connectionString = connectionType switch
|
||||
{
|
||||
LightningConnectionType.Charge =>
|
||||
$"type=charge;server={Server.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true",
|
||||
LightningConnectionType.CLightning =>
|
||||
$"type=clightning;server={((CLightningClient)Server.MerchantLightningD).Address.AbsoluteUri}",
|
||||
LightningConnectionType.LndREST =>
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
@ -129,16 +130,20 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("There are no forms yet.", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Id("CreateForm")).Click();
|
||||
s.Driver.FindElement(By.Name("Name")).SendKeys("Custom Form 1");
|
||||
s.Driver.FindElement((By.CssSelector("[data-form-template='email']"))).Click();
|
||||
var emailtemplate = s.Driver.FindElement(By.Name("FormConfig")).GetAttribute("value");
|
||||
Assert.Contains("buyerEmail", emailtemplate);
|
||||
s.Driver.FindElement(By.Id("ApplyEmailTemplate")).Click();
|
||||
|
||||
s.Driver.FindElement(By.Id("CodeTabButton")).Click();
|
||||
s.Driver.WaitForElement(By.Id("CodeTabPane"));
|
||||
|
||||
var config = s.Driver.FindElement(By.Name("FormConfig")).GetAttribute("value");
|
||||
Assert.Contains("buyerEmail", config);
|
||||
|
||||
s.Driver.FindElement(By.Name("FormConfig")).Clear();
|
||||
s.Driver.FindElement(By.Name("FormConfig"))
|
||||
.SendKeys(emailtemplate.Replace("Enter your email", "CustomFormInputTest"));
|
||||
.SendKeys(config.Replace("Enter your email", "CustomFormInputTest"));
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.Driver.FindElement(By.Id("ViewForm")).Click();
|
||||
|
||||
|
||||
var formurl = s.Driver.Url;
|
||||
Assert.Contains("CustomFormInputTest", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Name("buyerEmail")).SendKeys("aa@aa.com");
|
||||
@ -157,12 +162,16 @@ namespace BTCPayServer.Tests
|
||||
Assert.DoesNotContain("Custom Form 1", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Id("CreateForm")).Click();
|
||||
s.Driver.FindElement(By.Name("Name")).SendKeys("Custom Form 2");
|
||||
s.Driver.FindElement((By.CssSelector("[data-form-template='email']"))).Click();
|
||||
s.Driver.FindElement(By.Id("ApplyEmailTemplate")).Click();
|
||||
|
||||
s.Driver.FindElement(By.Id("CodeTabButton")).Click();
|
||||
s.Driver.WaitForElement(By.Id("CodeTabPane"));
|
||||
|
||||
s.Driver.SetCheckbox(By.Name("Public"), true);
|
||||
|
||||
s.Driver.FindElement(By.Name("FormConfig")).Clear();
|
||||
s.Driver.FindElement(By.Name("FormConfig"))
|
||||
.SendKeys(emailtemplate.Replace("Enter your email", "CustomFormInputTest2"));
|
||||
.SendKeys(config.Replace("Enter your email", "CustomFormInputTest2"));
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.Driver.FindElement(By.Id("ViewForm")).Click();
|
||||
formurl = s.Driver.Url;
|
||||
@ -600,7 +609,7 @@ namespace BTCPayServer.Tests
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
s.Driver.FindElement(By.Id("receipt-btn")).Click();
|
||||
s.Driver.FindElement(By.Id("ReceiptLink")).Click();
|
||||
});
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
@ -612,14 +621,13 @@ namespace BTCPayServer.Tests
|
||||
|
||||
await s.Server.PayTester.InvoiceRepository.MarkInvoiceStatus(i, InvoiceStatus.Settled);
|
||||
|
||||
TestUtils.Eventually(() => s.Driver.FindElement(By.Id("receipt-btn")).Click());
|
||||
TestUtils.Eventually(() => s.Driver.FindElement(By.Id("ReceiptLink")).Click());
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
Assert.DoesNotContain("invoice-unsettled", s.Driver.PageSource);
|
||||
Assert.DoesNotContain("invoice-processing", s.Driver.PageSource);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -630,21 +638,24 @@ namespace BTCPayServer.Tests
|
||||
s.RegisterNewUser();
|
||||
s.GoToUrl("/");
|
||||
|
||||
// verify redirected to create store page
|
||||
Assert.EndsWith("/stores/create", s.Driver.Url);
|
||||
Assert.Contains("Create your first store", s.Driver.PageSource);
|
||||
Assert.Contains("To start accepting payments, set up a store.", s.Driver.PageSource);
|
||||
Assert.False(s.Driver.PageSource.Contains("id=\"StoreSelectorDropdown\""), "Store selector dropdown should not be present");
|
||||
Assert.True(s.Driver.PageSource.Contains("id=\"StoreSelectorCreate\""), "Store selector create button should be present");
|
||||
|
||||
// verify steps for store creation are displayed correctly
|
||||
s.Driver.FindElement(By.Id("SetupGuide-Store")).Click();
|
||||
Assert.Contains("/stores/create", s.Driver.Url);
|
||||
|
||||
(_, string storeId) = s.CreateNewStore();
|
||||
|
||||
// should redirect to store
|
||||
// should redirect to first store
|
||||
s.GoToUrl("/");
|
||||
|
||||
Assert.Contains($"/stores/{storeId}", s.Driver.Url);
|
||||
Assert.True(s.Driver.PageSource.Contains("id=\"StoreSelectorDropdown\""), "Store selector dropdown should be present");
|
||||
Assert.True(s.Driver.PageSource.Contains("id=\"SetupGuide\""), "Store setup guide should be present");
|
||||
|
||||
s.GoToUrl("/stores/create");
|
||||
Assert.Contains("Create a new store", s.Driver.PageSource);
|
||||
Assert.DoesNotContain("Create your first store", s.Driver.PageSource);
|
||||
Assert.DoesNotContain("To start accepting payments, set up a store.", s.Driver.PageSource);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -718,8 +729,8 @@ namespace BTCPayServer.Tests
|
||||
Assert.DoesNotContain(invoiceId, s.Driver.PageSource);
|
||||
|
||||
// unarchive via list
|
||||
s.Driver.FindElement(By.Id("SearchOptionsToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("SearchOptionsIncludeArchived")).Click();
|
||||
s.Driver.FindElement(By.Id("StatusOptionsToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("StatusOptionsIncludeArchived")).Click();
|
||||
Assert.Contains(invoiceId, s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.CssSelector($".selector[value=\"{invoiceId}\"]")).Click();
|
||||
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
|
||||
@ -954,7 +965,7 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("ToggleRawEditor")).Click();
|
||||
|
||||
var template = s.Driver.FindElement(By.Id("Template")).GetAttribute("value");
|
||||
Assert.Contains("buyButtonText: Take my money", template);
|
||||
Assert.Contains("\"buyButtonText\":\"Take my money\"", template);
|
||||
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
@ -1731,11 +1742,12 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
Assert.Contains("transaction-label", s.Driver.PageSource);
|
||||
var labels = s.Driver.FindElements(By.CssSelector("#WalletTransactionsList tr:first-child div.transaction-label"));
|
||||
Assert.Equal(2, labels.Count);
|
||||
Assert.Contains(labels, element => element.Text == "payout");
|
||||
Assert.Contains(labels, element => element.Text == "pull-payment");
|
||||
});
|
||||
var labels = s.Driver.FindElements(By.CssSelector("#WalletTransactionsList tr:first-child div.transaction-label"));
|
||||
Assert.Equal(2, labels.Count);
|
||||
Assert.Contains(labels, element => element.Text == "payout");
|
||||
Assert.Contains(labels, element => element.Text == "pull-payment");
|
||||
|
||||
|
||||
s.GoToStore(s.StoreId, StoreNavPages.Payouts);
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.InProgress}-view")).Click();
|
||||
@ -2437,10 +2449,11 @@ retry:
|
||||
_ = await request.SendChallenge(linkingKey, new HttpClient());
|
||||
TestUtils.Eventually(() => s.FindAlertMessage());
|
||||
|
||||
s.CreateNewStore(); // create a store to prevent redirect after login
|
||||
s.Logout();
|
||||
s.LogIn(user, "123456");
|
||||
var section = s.Driver.FindElement(By.Id("lnurlauth-section"));
|
||||
links = section.FindElements(By.CssSelector(".tab-content a")).Select(element => element.GetAttribute("href"));
|
||||
links = section.FindElements(By.CssSelector(".tab-content a")).Select(element => element.GetAttribute("href")).ToList();
|
||||
Assert.Equal(2, links.Count());
|
||||
prevEndpoint = null;
|
||||
foreach (string link in links)
|
||||
@ -2454,9 +2467,148 @@ retry:
|
||||
_ = await request.SendChallenge(linkingKey, new HttpClient());
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
Assert.Equal(s.Driver.Url, s.ServerUri.ToString());
|
||||
Assert.StartsWith(s.ServerUri.ToString(), s.Driver.Url);
|
||||
});
|
||||
}
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
public async Task CanUseRoleManager()
|
||||
{
|
||||
using var s = CreateSeleniumTester(newDb: true);
|
||||
await s.StartAsync();
|
||||
var user = s.RegisterNewUser(true);
|
||||
s.GoToServer(ServerNavPages.Roles);
|
||||
var existingServerRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
|
||||
Assert.Equal(3, existingServerRoles.Count);
|
||||
IWebElement ownerRow = null;
|
||||
IWebElement guestRow = null;
|
||||
foreach (var roleItem in existingServerRoles)
|
||||
{
|
||||
if (roleItem.Text.Contains("owner", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
ownerRow = roleItem;
|
||||
}
|
||||
else if (roleItem.Text.Contains("guest", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
guestRow = roleItem;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.NotNull(ownerRow);
|
||||
Assert.NotNull(guestRow);
|
||||
|
||||
var ownerBadges = ownerRow.FindElements(By.CssSelector(".badge"));
|
||||
Assert.Contains(ownerBadges, element => element.Text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
|
||||
Assert.Contains(ownerBadges, element => element.Text.Equals("Server-wide", StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
var guestBadges = guestRow.FindElements(By.CssSelector(".badge"));
|
||||
Assert.DoesNotContain(guestBadges, element => element.Text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
|
||||
Assert.Contains(guestBadges, element => element.Text.Equals("Server-wide", StringComparison.InvariantCultureIgnoreCase));
|
||||
guestRow.FindElement(By.Id("SetDefault")).Click();
|
||||
s.FindAlertMessage();
|
||||
|
||||
existingServerRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
|
||||
foreach (var roleItem in existingServerRoles)
|
||||
{
|
||||
if (roleItem.Text.Contains("owner", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
ownerRow = roleItem;
|
||||
}
|
||||
else if (roleItem.Text.Contains("guest", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
guestRow = roleItem;
|
||||
}
|
||||
}
|
||||
guestBadges = guestRow.FindElements(By.CssSelector(".badge"));
|
||||
Assert.Contains(guestBadges, element => element.Text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
ownerBadges = ownerRow.FindElements(By.CssSelector(".badge"));
|
||||
Assert.DoesNotContain(ownerBadges, element => element.Text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
|
||||
ownerRow.FindElement(By.Id("SetDefault")).Click();
|
||||
s.FindAlertMessage();
|
||||
|
||||
|
||||
|
||||
s.CreateNewStore();
|
||||
s.GoToStore(StoreNavPages.Roles);
|
||||
var existingStoreRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
|
||||
Assert.Equal(3, existingStoreRoles.Count);
|
||||
Assert.Equal(2, existingStoreRoles.Count(element => element.Text.Contains("Server-wide", StringComparison.InvariantCultureIgnoreCase)));
|
||||
|
||||
foreach (var roleItem in existingStoreRoles)
|
||||
{
|
||||
if (roleItem.Text.Contains("owner", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
ownerRow = roleItem;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ownerRow.FindElement(By.LinkText("Remove")).Click();
|
||||
Assert.DoesNotContain("ConfirmContinue", s.Driver.PageSource);
|
||||
s.Driver.Navigate().Back();
|
||||
existingStoreRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
|
||||
foreach (var roleItem in existingStoreRoles)
|
||||
{
|
||||
if (roleItem.Text.Contains("guest", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
guestRow = roleItem;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
guestRow.FindElement(By.LinkText("Remove")).Click();
|
||||
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
|
||||
s.FindAlertMessage();
|
||||
|
||||
s.GoToStore(StoreNavPages.Roles);
|
||||
s.Driver.FindElement(By.Id("CreateRole")).Click();
|
||||
|
||||
Assert.Contains("Create role", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
s.Driver.FindElement(By.Id("Role")).SendKeys("store role");
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
s.FindAlertMessage();
|
||||
|
||||
existingStoreRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
|
||||
foreach (var roleItem in existingStoreRoles)
|
||||
{
|
||||
if (roleItem.Text.Contains("store role", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
guestRow = roleItem;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
guestBadges = guestRow.FindElements(By.CssSelector(".badge"));
|
||||
Assert.DoesNotContain(guestBadges, element => element.Text.Equals("server-wide", StringComparison.InvariantCultureIgnoreCase));
|
||||
s.GoToStore(StoreNavPages.Users);
|
||||
var options = s.Driver.FindElements(By.CssSelector("#Role option"));
|
||||
Assert.Equal(2, options.Count);
|
||||
Assert.Contains(options, element => element.Text.Equals("store role", StringComparison.InvariantCultureIgnoreCase));
|
||||
s.CreateNewStore();
|
||||
s.GoToStore(StoreNavPages.Roles);
|
||||
existingStoreRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
|
||||
Assert.Equal(2, existingStoreRoles.Count);
|
||||
Assert.Equal(1, existingStoreRoles.Count(element => element.Text.Contains("Server-wide", StringComparison.InvariantCultureIgnoreCase)));
|
||||
Assert.Equal(0, existingStoreRoles.Count(element => element.Text.Contains("store role", StringComparison.InvariantCultureIgnoreCase)));
|
||||
s.GoToStore(StoreNavPages.Users);
|
||||
options = s.Driver.FindElements(By.CssSelector("#Role option"));
|
||||
Assert.Single(options);
|
||||
Assert.DoesNotContain(options, element => element.Text.Equals("store role", StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
|
||||
s.GoToStore(StoreNavPages.Roles);
|
||||
s.Driver.FindElement(By.Id("CreateRole")).Click();
|
||||
s.Driver.FindElement(By.Id("Role")).SendKeys("Malice");
|
||||
|
||||
s.Driver.ExecuteJavaScript($"document.getElementById('Policies')['{Policies.CanModifyServerSettings}']=new Option('{Policies.CanModifyServerSettings}', '{Policies.CanModifyServerSettings}', true,true);");
|
||||
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
s.FindAlertMessage();
|
||||
Assert.Contains("Malice",s.Driver.PageSource);
|
||||
Assert.DoesNotContain(Policies.CanModifyServerSettings,s.Driver.PageSource);
|
||||
}
|
||||
|
||||
private static void CanBrowseContent(SeleniumTester s)
|
||||
{
|
||||
|
@ -92,7 +92,7 @@ namespace BTCPayServer.Tests
|
||||
#endif
|
||||
public void ActivateLightning()
|
||||
{
|
||||
ActivateLightning(LightningConnectionType.Charge);
|
||||
ActivateLightning(LightningConnectionType.CLightning);
|
||||
}
|
||||
public void ActivateLightning(LightningConnectionType internalNode)
|
||||
{
|
||||
@ -109,14 +109,7 @@ namespace BTCPayServer.Tests
|
||||
string connectionString = null;
|
||||
if (connectionType is null)
|
||||
return LightningSupportedPaymentMethod.InternalNode;
|
||||
if (connectionType == LightningConnectionType.Charge)
|
||||
{
|
||||
if (isMerchant)
|
||||
connectionString = $"type=charge;server={MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true";
|
||||
else
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
else if (connectionType == LightningConnectionType.CLightning)
|
||||
if (connectionType == LightningConnectionType.CLightning)
|
||||
{
|
||||
if (isMerchant)
|
||||
connectionString = "type=clightning;server=" +
|
||||
@ -194,7 +187,8 @@ namespace BTCPayServer.Tests
|
||||
tcs.TrySetResult(evt);
|
||||
}
|
||||
});
|
||||
await action.Invoke();
|
||||
if (action != null)
|
||||
await action.Invoke();
|
||||
var result = await tcs.Task;
|
||||
sub.Dispose();
|
||||
return result;
|
||||
@ -247,6 +241,8 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public List<string> Stores { get; internal set; } = new List<string>();
|
||||
public bool DeleteStore { get; set; } = true;
|
||||
public BTCPayNetworkBase DefaultNetwork => NetworkProvider.DefaultNetwork;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var r in this.Resources)
|
||||
|
@ -277,7 +277,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public bool IsAdmin { get; internal set; }
|
||||
|
||||
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true)
|
||||
public void RegisterLightningNode(string cryptoCode, LightningConnectionType? connectionType = null, bool isMerchant = true)
|
||||
{
|
||||
RegisterLightningNodeAsync(cryptoCode, connectionType, isMerchant).GetAwaiter().GetResult();
|
||||
}
|
||||
@ -470,7 +470,10 @@ namespace BTCPayServer.Tests
|
||||
var req = await _server.GetNextRequest(cancellation);
|
||||
var bytes = await req.Request.Body.ReadBytesAsync((int)req.Request.Headers.ContentLength);
|
||||
var callback = Encoding.UTF8.GetString(bytes);
|
||||
_webhookEvents.Add(JsonConvert.DeserializeObject<WebhookInvoiceEvent>(callback));
|
||||
lock (_webhookEvents)
|
||||
{
|
||||
_webhookEvents.Add(JsonConvert.DeserializeObject<WebhookInvoiceEvent>(callback));
|
||||
}
|
||||
req.Response.StatusCode = 200;
|
||||
_server.Done();
|
||||
}
|
||||
@ -487,18 +490,21 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
int retry = 0;
|
||||
retry:
|
||||
foreach (var evt in WebhookEvents)
|
||||
lock (WebhookEvents)
|
||||
{
|
||||
if (evt.Type == eventType)
|
||||
foreach (var evt in WebhookEvents)
|
||||
{
|
||||
var typedEvt = evt.ReadAs<TEvent>();
|
||||
try
|
||||
{
|
||||
assert(typedEvt);
|
||||
return typedEvt;
|
||||
}
|
||||
catch (XunitException)
|
||||
if (evt.Type == eventType)
|
||||
{
|
||||
var typedEvt = evt.ReadAs<TEvent>();
|
||||
try
|
||||
{
|
||||
assert(typedEvt);
|
||||
return typedEvt;
|
||||
}
|
||||
catch (XunitException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -540,12 +546,12 @@ retry:
|
||||
public async Task AddGuest(string userId)
|
||||
{
|
||||
var repo = this.parent.PayTester.GetService<StoreRepository>();
|
||||
await repo.AddStoreUser(StoreId, userId, "Guest");
|
||||
await repo.AddStoreUser(StoreId, userId, StoreRoleId.Guest);
|
||||
}
|
||||
public async Task AddOwner(string userId)
|
||||
{
|
||||
var repo = this.parent.PayTester.GetService<StoreRepository>();
|
||||
await repo.AddStoreUser(StoreId, userId, "Owner");
|
||||
await repo.AddStoreUser(StoreId, userId, StoreRoleId.Owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,7 +112,14 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(500, cts.Token);
|
||||
bool timeout =false;
|
||||
try
|
||||
{
|
||||
await Task.Delay(500, cts.Token);
|
||||
}
|
||||
catch { timeout = true; }
|
||||
if (timeout)
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -306,6 +306,8 @@ retry:
|
||||
foreach ((CurrencyPair key, Task<RateResult> value) in result)
|
||||
{
|
||||
var rateResult = value.GetAwaiter().GetResult();
|
||||
if (key.ToString() == "BTG_USD")
|
||||
continue; // shitcoin not supported by bitfinex anymore
|
||||
TestLogs.LogInformation($"Testing {key}");
|
||||
if (brokenShitcoins.Contains(key.ToString()))
|
||||
continue;
|
||||
@ -358,6 +360,11 @@ retry:
|
||||
version = Regex.Match(actual, "Original file: /npm/dom-confetti@([0-9]+.[0-9]+.[0-9]+)/lib/main.js").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}/lib/main.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-sortable", "sortable.min.js").Trim();
|
||||
version = Regex.Match(actual, "Sortable ([0-9]+.[0-9]+.[0-9]+) ").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://unpkg.com/sortablejs@{version}/Sortable.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
string GetFileContent(params string[] path)
|
||||
|
@ -466,14 +466,6 @@ namespace BTCPayServer.Tests
|
||||
await ProcessLightningPayment(LightningConnectionType.CLightning);
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
public async Task CanSendLightningPaymentCharge()
|
||||
{
|
||||
await ProcessLightningPayment(LightningConnectionType.Charge);
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
@ -1634,7 +1626,7 @@ namespace BTCPayServer.Tests
|
||||
var user = tester.NewAccount();
|
||||
var cryptoCode = "BTC";
|
||||
user.GrantAccess(true);
|
||||
user.RegisterLightningNode(cryptoCode, LightningConnectionType.Charge);
|
||||
user.RegisterLightningNode(cryptoCode);
|
||||
user.SetLNUrl(cryptoCode, false);
|
||||
var vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
|
||||
var criteria = Assert.Single(vm.PaymentMethodCriteria);
|
||||
@ -1654,7 +1646,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType);
|
||||
|
||||
// Activating LNUrl, we should still have only 1 payment criteria that can be set.
|
||||
user.RegisterLightningNode(cryptoCode, LightningConnectionType.Charge);
|
||||
user.RegisterLightningNode(cryptoCode);
|
||||
user.SetLNUrl(cryptoCode, true);
|
||||
vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
|
||||
criteria = Assert.Single(vm.PaymentMethodCriteria);
|
||||
@ -1970,7 +1962,8 @@ namespace BTCPayServer.Tests
|
||||
Assert.Empty(appList2.Apps);
|
||||
Assert.Equal("test", appList.Apps[0].AppName);
|
||||
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
|
||||
Assert.True(app.IsOwner);
|
||||
|
||||
Assert.True(app.Role.ToPermissionSet(app.StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
|
||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
|
||||
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
|
||||
@ -2154,7 +2147,7 @@ namespace BTCPayServer.Tests
|
||||
txFee = localInvoice.BtcDue - invoice.BtcDue;
|
||||
Assert.Equal("paidPartial", localInvoice.ExceptionStatus.ToString());
|
||||
Assert.Equal(1, localInvoice.CryptoInfo[0].TxCount);
|
||||
Assert.NotEqual(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //New address
|
||||
Assert.Equal(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //Same address
|
||||
Assert.True(IsMapped(invoice, ctx));
|
||||
Assert.True(IsMapped(localInvoice, ctx));
|
||||
|
||||
|
@ -24,7 +24,6 @@ services:
|
||||
TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none}
|
||||
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
|
||||
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
|
||||
TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true"
|
||||
TEST_MERCHANTLND: "http://lnd:lnd@merchant_lnd:8080/"
|
||||
TESTS_INCONTAINER: "true"
|
||||
TESTS_SSHCONNECTION: "root@sshd:22"
|
||||
@ -56,7 +55,6 @@ services:
|
||||
- postgres
|
||||
- customer_lightningd
|
||||
- merchant_lightningd
|
||||
- lightning-charged
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
- sshd
|
||||
@ -75,7 +73,7 @@ services:
|
||||
- "sshd_datadir:/root/.ssh"
|
||||
|
||||
devlnd:
|
||||
image: btcpayserver/bitcoin:24.0
|
||||
image: btcpayserver/bitcoin:25.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -89,14 +87,19 @@ services:
|
||||
- postgres
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
|
||||
selenium:
|
||||
image: selenium/standalone-chrome:101.0
|
||||
extra_hosts:
|
||||
- "tests:172.23.0.18"
|
||||
expose:
|
||||
- "4444"
|
||||
extra_hosts:
|
||||
- "tests:172.18.0.18"
|
||||
networks:
|
||||
default:
|
||||
custom:
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.3.58
|
||||
image: nicolasdorier/nbxplorer:2.3.63
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -132,7 +135,7 @@ services:
|
||||
|
||||
bitcoind:
|
||||
restart: unless-stopped
|
||||
image: btcpayserver/bitcoin:24.0
|
||||
image: btcpayserver/bitcoin:25.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -160,7 +163,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v23.02-1-dev
|
||||
image: btcpayserver/lightning:v23.05-dev
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -186,30 +189,8 @@ services:
|
||||
depends_on:
|
||||
- bitcoind
|
||||
|
||||
lightning-charged:
|
||||
image: shesek/lightning-charge:0.4.23-1-standalone
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NETWORK: regtest
|
||||
API_TOKEN: foiewnccewuify
|
||||
BITCOIND_RPCCONNECT: bitcoind
|
||||
LN_NET_PATH: /etc/lightning
|
||||
LN_NET: /etc/lightning
|
||||
volumes:
|
||||
- "bitcoin_datadir:/etc/bitcoin"
|
||||
- "lightning_charge_datadir:/data"
|
||||
- "merchant_lightningd_datadir:/etc/lightning"
|
||||
expose:
|
||||
- "9112" # Charge
|
||||
- "9735" # Lightning
|
||||
ports:
|
||||
- "54938:9112" # Charge
|
||||
depends_on:
|
||||
- bitcoind
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v23.02-1-dev
|
||||
image: btcpayserver/lightning:v23.05-dev
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
@ -243,7 +224,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.16.0-beta
|
||||
image: btcpayserver/lnd:v0.16.2-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -278,7 +259,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.16.0-beta
|
||||
image: btcpayserver/lnd:v0.16.2-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -22,7 +22,6 @@ services:
|
||||
TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none}
|
||||
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
|
||||
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
|
||||
TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true"
|
||||
TEST_MERCHANTLND: "http://lnd:lnd@merchant_lnd:8080/"
|
||||
TESTS_INCONTAINER: "true"
|
||||
TESTS_SSHCONNECTION: "root@sshd:22"
|
||||
@ -54,7 +53,6 @@ services:
|
||||
- postgres
|
||||
- customer_lightningd
|
||||
- merchant_lightningd
|
||||
- lightning-charged
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
- sshd
|
||||
@ -72,28 +70,33 @@ services:
|
||||
- "sshd_datadir:/root/.ssh"
|
||||
|
||||
devlnd:
|
||||
image: btcpayserver/bitcoin:24.0
|
||||
image: btcpayserver/bitcoin:25.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
deprecatedrpc=signrawtransaction
|
||||
connect=bitcoind:39388
|
||||
rpcallowip=0.0.0.0/0
|
||||
fallbackfee=0.0002
|
||||
rpcallowip=0.0.0.0/0
|
||||
depends_on:
|
||||
- nbxplorer
|
||||
- postgres
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
|
||||
selenium:
|
||||
image: selenium/standalone-chrome:101.0
|
||||
extra_hosts:
|
||||
- "tests:172.18.0.18"
|
||||
- "tests:172.23.0.18"
|
||||
expose:
|
||||
- "4444"
|
||||
networks:
|
||||
default:
|
||||
custom:
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.3.58
|
||||
image: nicolasdorier/nbxplorer:2.3.63
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -118,7 +121,7 @@ services:
|
||||
|
||||
bitcoind:
|
||||
restart: unless-stopped
|
||||
image: btcpayserver/bitcoin:24.0
|
||||
image: btcpayserver/bitcoin:25.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -146,7 +149,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v23.02-1-dev
|
||||
image: btcpayserver/lightning:v23.05-dev
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -172,30 +175,8 @@ services:
|
||||
depends_on:
|
||||
- bitcoind
|
||||
|
||||
lightning-charged:
|
||||
image: shesek/lightning-charge:0.4.23-1-standalone
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NETWORK: regtest
|
||||
API_TOKEN: foiewnccewuify
|
||||
BITCOIND_RPCCONNECT: bitcoind
|
||||
LN_NET_PATH: /etc/lightning
|
||||
LN_NET: /etc/lightning
|
||||
volumes:
|
||||
- "bitcoin_datadir:/etc/bitcoin"
|
||||
- "lightning_charge_datadir:/data"
|
||||
- "merchant_lightningd_datadir:/etc/lightning"
|
||||
expose:
|
||||
- "9112" # Charge
|
||||
- "9735" # Lightning
|
||||
ports:
|
||||
- "54938:9112" # Charge
|
||||
depends_on:
|
||||
- bitcoind
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v23.02-1-dev
|
||||
image: btcpayserver/lightning:v23.05-dev
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
@ -230,7 +211,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.16.0-beta
|
||||
image: btcpayserver/lnd:v0.16.2-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -267,7 +248,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.16.0-beta
|
||||
image: btcpayserver/lnd:v0.16.2-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -45,9 +45,10 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="YamlDotNet" Version="8.0.0" />
|
||||
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.23" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.26" />
|
||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="Fido2" Version="2.0.2" />
|
||||
@ -75,7 +76,6 @@
|
||||
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.12.1" />
|
||||
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.12.1" />
|
||||
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.9" />
|
||||
</ItemGroup>
|
||||
@ -111,9 +111,6 @@
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_screen-reader.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_stacked.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_variables.scss" />
|
||||
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.compatibility.js" />
|
||||
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.js" />
|
||||
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.min.js" />
|
||||
<None Include="wwwroot\vendor\jquery\jquery.js" />
|
||||
<None Include="wwwroot\vendor\jquery\jquery.min.js" />
|
||||
</ItemGroup>
|
||||
|
@ -48,7 +48,7 @@
|
||||
<span class="app-item-point ct-point"></span>
|
||||
@entry.Title
|
||||
</span>
|
||||
<span class="app-item-value">
|
||||
<span class="app-item-value" data-sensitive>
|
||||
<span class="text-muted">@entry.SalesCount @($"{label}{(entry.SalesCount == 1 ? "" : "s")}"),</span>
|
||||
@entry.TotalFormatted
|
||||
</span>
|
||||
|
@ -1,6 +1,5 @@
|
||||
@using BTCPayServer.Views.Server
|
||||
@using BTCPayServer.Views.Stores
|
||||
@using BTCPayServer.Views.Apps
|
||||
@using BTCPayServer.Views.Invoice
|
||||
@using BTCPayServer.Views.Manage
|
||||
@using BTCPayServer.Views.PaymentRequest
|
||||
@ -178,7 +177,7 @@
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item" permission="@Policies.CanModifyServerSettings">
|
||||
<a asp-area="" asp-controller="UIServer" asp-action="ListPlugins" class="nav-link @ViewData.IsActivePage(ServerNavPages.Plugins)" id="Nav-ManagePlugins">
|
||||
<vc:icon symbol="plugin"/>
|
||||
<vc:icon symbol="manage-plugins"/>
|
||||
<span>Manage Plugins</span>
|
||||
</a>
|
||||
</li>
|
||||
@ -239,7 +238,7 @@
|
||||
<span>Account</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu py-0 w-100" aria-labelledby="Nav-Account">
|
||||
<li class="p-3">
|
||||
<li class="p-3 border-bottom">
|
||||
<strong class="d-block text-truncate" style="max-width:195px">@User.Identity.Name</strong>
|
||||
@if (User.IsInRole(Roles.ServerAdmin))
|
||||
{
|
||||
@ -248,10 +247,19 @@
|
||||
</li>
|
||||
@if (!Theme.CustomTheme)
|
||||
{
|
||||
<li class="border-top py-1 px-3">
|
||||
<vc:theme-switch css-class="nav-link"/>
|
||||
<li class="py-1 px-3">
|
||||
<vc:theme-switch css-class="nav-link pb-0"/>
|
||||
</li>
|
||||
}
|
||||
<li class="py-1 px-3">
|
||||
<label class="d-flex align-items-center justify-content-between gap-3 nav-link">
|
||||
<span class="fw-semibold">Hide Sensitive Info</span>
|
||||
<input id="HideSensitiveInfo" name="HideSensitiveInfo" type="checkbox" class="btcpay-toggle" />
|
||||
</label>
|
||||
<script>
|
||||
document.getElementById('HideSensitiveInfo').checked = window.localStorage.getItem('btcpay-hide-sensitive-info') === 'true';
|
||||
</script>
|
||||
</li>
|
||||
<li class="border-top py-1 px-3">
|
||||
<a asp-area="" asp-controller="UIManage" asp-action="Index" class="nav-link @ViewData.IsActiveCategory(typeof(ManageNavPages))" id="Nav-ManageAccount">
|
||||
<span>Manage Account</span>
|
||||
|
@ -72,7 +72,6 @@ namespace BTCPayServer.Components.MainNav
|
||||
vm.Apps = apps.Select(a => new StoreApp
|
||||
{
|
||||
Id = a.Id,
|
||||
IsOwner = a.IsOwner,
|
||||
AppName = a.AppName,
|
||||
AppType = a.AppType
|
||||
}).ToList();
|
||||
|
@ -20,6 +20,5 @@ namespace BTCPayServer.Components.MainNav
|
||||
public string Id { get; set; }
|
||||
public string AppName { get; set; }
|
||||
public string AppType { get; set; }
|
||||
public bool IsOwner { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -23,16 +23,18 @@
|
||||
@if (Model.Balance.OffchainBalance != null)
|
||||
{
|
||||
<div class="balance">
|
||||
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOffchain">@Model.TotalOffchain</h3>
|
||||
<span class="text-secondary fw-semibold text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> in channels
|
||||
</span>
|
||||
<div class="d-flex align-items-baseline gap-1">
|
||||
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOffchain" data-sensitive>@Model.TotalOffchain</h3>
|
||||
<span class="text-secondary fw-semibold text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> in channels
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="balance-details collapse" id="balanceDetailsOffchain">
|
||||
@if (Model.Balance.OffchainBalance.Opening != null)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Opening">
|
||||
<div class="mt-2 d-flex align-items-baseline gap-1">
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Opening" data-sensitive>
|
||||
@Model.Balance.OffchainBalance.Opening
|
||||
</span>
|
||||
<span class="text-secondary text-nowrap">
|
||||
@ -42,8 +44,8 @@
|
||||
}
|
||||
@if (Model.Balance.OffchainBalance.Local != null)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Local">
|
||||
<div class="mt-2 d-flex align-items-baseline gap-1">
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Local" data-sensitive>
|
||||
@Model.Balance.OffchainBalance.Local
|
||||
</span>
|
||||
<span class="text-secondary text-nowrap">
|
||||
@ -53,8 +55,8 @@
|
||||
}
|
||||
@if (Model.Balance.OffchainBalance.Remote != null)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Remote">
|
||||
<div class="mt-2 d-flex align-items-baseline gap-1">
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Remote" data-sensitive>
|
||||
@Model.Balance.OffchainBalance.Remote
|
||||
</span>
|
||||
<span class="text-secondary text-nowrap">
|
||||
@ -64,8 +66,8 @@
|
||||
}
|
||||
@if (Model.Balance.OffchainBalance.Closing != null)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Closing">
|
||||
<div class="mt-2 d-flex align-items-baseline gap-1">
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Closing" data-sensitive>
|
||||
@Model.Balance.OffchainBalance.Closing
|
||||
</span>
|
||||
<span class="text-secondary text-nowrap">
|
||||
@ -79,15 +81,17 @@
|
||||
@if (Model.Balance.OnchainBalance != null)
|
||||
{
|
||||
<div class="balance">
|
||||
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOnchain">@Model.TotalOnchain</h3>
|
||||
<span class="text-secondary fw-semibold text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> on-chain
|
||||
</span>
|
||||
<div class="d-flex align-items-baseline gap-1">
|
||||
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOnchain" data-sensitive>@Model.TotalOnchain</h3>
|
||||
<span class="text-secondary fw-semibold text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> on-chain
|
||||
</span>
|
||||
</div>
|
||||
<div class="balance-details collapse" id="balanceDetailsOnchain">
|
||||
@if (Model.Balance.OnchainBalance.Confirmed != null)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Confirmed">
|
||||
<div class="mt-2 d-flex align-items-baseline gap-1">
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Confirmed" data-sensitive>
|
||||
@Model.Balance.OnchainBalance.Confirmed
|
||||
</span>
|
||||
<span class="text-secondary text-nowrap">
|
||||
@ -97,8 +101,8 @@
|
||||
}
|
||||
@if (Model.Balance.OnchainBalance.Unconfirmed != null)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Unconfirmed">
|
||||
<div class="mt-2 d-flex align-items-baseline gap-1">
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Unconfirmed" data-sensitive>
|
||||
@Model.Balance.OnchainBalance.Unconfirmed
|
||||
</span>
|
||||
<span class="text-secondary text-nowrap">
|
||||
@ -108,8 +112,8 @@
|
||||
}
|
||||
@if (Model.Balance.OnchainBalance.Reserved != null)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Reserved">
|
||||
<div class="mt-2 d-flex align-items-baseline gap-1">
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Reserved" data-sensitive>
|
||||
@Model.Balance.OnchainBalance.Reserved
|
||||
</span>
|
||||
<span class="text-secondary text-nowrap">
|
||||
|
@ -65,7 +65,9 @@
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-end">@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</td>
|
||||
<td class="text-end">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
|
@ -72,11 +72,15 @@
|
||||
</td>
|
||||
@if (tx.Positive)
|
||||
{
|
||||
<td class="text-end text-success">@DisplayFormatter.Currency(tx.Balance, tx.Currency)</td>
|
||||
<td class="text-end text-success">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
|
||||
</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td class="text-end text-danger">@DisplayFormatter.Currency(tx.Balance, tx.Currency)</td>
|
||||
<td class="text-end text-danger">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Abstractions.Contracts
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Services
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
@inject BTCPayServerEnvironment Env
|
||||
@inject IFileService FileService
|
||||
@model BTCPayServer.Components.StoreSelector.StoreSelectorViewModel
|
||||
@ -29,18 +29,14 @@
|
||||
{
|
||||
<a asp-controller="UIHome" asp-action="Index" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
|
||||
}
|
||||
else if (Model.CurrentStoreIsOwner)
|
||||
{
|
||||
<a asp-controller="UIStores" asp-action="Dashboard" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
|
||||
<a asp-controller="UIStores" asp-action="Dashboard" permission="@Policies.CanModifyStoreSettings" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
|
||||
<a asp-controller="UIInvoice" asp-action="ListInvoices" not-permission="@Policies.CanModifyStoreSettings" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
|
||||
}
|
||||
|
||||
<div id="StoreSelector">
|
||||
@if (Model.Options.Any())
|
||||
{
|
||||
@if (Model.Options.Any())
|
||||
{
|
||||
<div id="StoreSelector">
|
||||
<div id="StoreSelectorDropdown" class="dropdown only-for-js">
|
||||
<button id="StoreSelectorToggle" class="btn btn-secondary dropdown-toggle rounded-pill px-3 @(Model.CurrentStoreId == null ? "empty-state" : "")" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
@if (!string.IsNullOrEmpty(Model.CurrentStoreLogoFileId))
|
||||
@ -72,9 +68,5 @@ else
|
||||
<li><a asp-controller="UIUserStores" asp-action="CreateStore" class="dropdown-item" id="StoreSelectorCreate">Create Store</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
else if (SignInManager.IsSignedIn(User))
|
||||
{
|
||||
<a asp-controller="UIUserStores" asp-action="CreateStore" class="btn btn-primary w-100 rounded-pill text-nowrap" id="StoreSelectorCreate">Create Store</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@ -38,13 +39,14 @@ namespace BTCPayServer.Components.StoreSelector
|
||||
.FirstOrDefault()?
|
||||
.Network.CryptoCode;
|
||||
var walletId = cryptoCode != null ? new WalletId(store.Id, cryptoCode) : null;
|
||||
var role = store.GetStoreRoleOfUser(userId);
|
||||
return new StoreSelectorOption
|
||||
{
|
||||
Text = store.StoreName,
|
||||
Value = store.Id,
|
||||
Selected = store.Id == currentStore?.Id,
|
||||
IsOwner = store.Role == StoreRoles.Owner,
|
||||
WalletId = walletId
|
||||
WalletId = walletId,
|
||||
IsOwner = role != null && role.Permissions.Contains(Policies.CanModifyStoreSettings)
|
||||
};
|
||||
})
|
||||
.OrderBy(s => s.Text)
|
||||
@ -57,7 +59,6 @@ namespace BTCPayServer.Components.StoreSelector
|
||||
Options = options,
|
||||
CurrentStoreId = currentStore?.Id,
|
||||
CurrentDisplayName = currentStore?.StoreName,
|
||||
CurrentStoreIsOwner = currentStore?.Role == StoreRoles.Owner,
|
||||
CurrentStoreLogoFileId = blob?.LogoFileId
|
||||
};
|
||||
|
||||
|
@ -8,7 +8,6 @@ namespace BTCPayServer.Components.StoreSelector
|
||||
public string CurrentStoreId { get; set; }
|
||||
public string CurrentStoreLogoFileId { get; set; }
|
||||
public string CurrentDisplayName { get; set; }
|
||||
public bool CurrentStoreIsOwner { get; set; }
|
||||
}
|
||||
|
||||
public class StoreSelectorOption
|
||||
|
@ -17,8 +17,8 @@
|
||||
<header class="mb-3">
|
||||
@if (Model.Balance != null)
|
||||
{
|
||||
<div class="balance">
|
||||
<h3 class="d-inline-block me-1" data-balance="@Model.Balance">@Model.Balance</h3>
|
||||
<div class="balance d-flex align-items-baseline gap-1">
|
||||
<h3 class="d-inline-block me-1" data-balance="@Model.Balance" data-sensitive>@Model.Balance</h3>
|
||||
<span class="text-secondary fw-semibold currency">@Model.CryptoCode</span>
|
||||
</div>
|
||||
}
|
||||
@ -92,8 +92,7 @@
|
||||
window.setTimeout(() => {
|
||||
const yLabels = [...document.querySelectorAll('.ct-label.ct-vertical.ct-start')];
|
||||
if (yLabels) {
|
||||
const factor = rate ? 6 : 8;
|
||||
const width = Math.max(...(yLabels.map(l => l.innerText.length * factor)));
|
||||
const width = Math.max(...(yLabels.map(l => l.innerText.length * 7.5)));
|
||||
const opts = Object.assign({}, renderOpts, {
|
||||
axisY: Object.assign({}, renderOpts.axisY, { offset: width })
|
||||
});
|
||||
|
@ -1,10 +1,29 @@
|
||||
@model BTCPayServer.Components.TruncateCenter.TruncateCenterViewModel
|
||||
<span class="truncate-center @Model.Classes">
|
||||
<span class="truncate-center-truncated" @(Model.Truncated != Model.Text ? $"data-bs-toggle=tooltip title={Model.Text}" : "")>@Model.Truncated</span>
|
||||
<span class="truncate-center-text">@Model.Text</span>
|
||||
@{
|
||||
var classes = string.IsNullOrEmpty(Model.Classes) ? string.Empty : Model.Classes.Trim();
|
||||
@if (Model.Copy) classes += " truncate-center--copy";
|
||||
@if (Model.Elastic) classes += " truncate-center--elastic";
|
||||
}
|
||||
<span class="truncate-center @classes">
|
||||
@if (Model.IsVue)
|
||||
{
|
||||
<span class="truncate-center-truncated" data-bs-toggle="tooltip" :title=@Safe.Json(Model.Text)>
|
||||
<span class="truncate-center-start" v-text=@Safe.Json(Model.Text)></span>
|
||||
<span class="truncate-center-end" v-text=@Safe.Json($"{Model.Text}.slice(-{Model.Padding})")></span>
|
||||
</span>
|
||||
<span class="truncate-center-text" v-text=@Safe.Json(Model.Text)></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="truncate-center-truncated" @(!string.IsNullOrEmpty(Model.Start) ? $"data-bs-toggle=tooltip title={Model.Text}" : "")>
|
||||
<span class="truncate-center-start">@(Model.Elastic ? Model.Text : $"{Model.Start}…")</span>
|
||||
<span class="truncate-center-end">@Model.End</span>
|
||||
</span>
|
||||
<span class="truncate-center-text">@Model.Text</span>
|
||||
}
|
||||
@if (Model.Copy)
|
||||
{
|
||||
<button type="button" class="btn btn-link p-0" data-clipboard="@Model.Text">
|
||||
<button type="button" class="btn btn-link p-0" @(Model.IsVue ? ":" : string.Empty)data-clipboard=@Safe.Json(Model.Text)>
|
||||
<vc:icon symbol="copy" />
|
||||
</button>
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ namespace BTCPayServer.Components.TruncateCenter;
|
||||
/// <returns>HTML with truncated string</returns>
|
||||
public class TruncateCenter : ViewComponent
|
||||
{
|
||||
public IViewComponentResult Invoke(string text, string link = null, string classes = null, int padding = 7, bool copy = true)
|
||||
public IViewComponentResult Invoke(string text, string link = null, string classes = null, int padding = 7, bool copy = true, bool elastic = false, bool isVue = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
|
||||
@ -23,11 +23,17 @@ public class TruncateCenter : ViewComponent
|
||||
{
|
||||
Classes = classes,
|
||||
Padding = padding,
|
||||
Elastic = elastic,
|
||||
IsVue = isVue,
|
||||
Copy = copy,
|
||||
Text = text,
|
||||
Link = link,
|
||||
Truncated = text.Length > 2 * padding ? $"{text[..padding]}…{text[^padding..]}" : text
|
||||
Link = link
|
||||
};
|
||||
if (!isVue && text.Length > 2 * padding)
|
||||
{
|
||||
vm.Start = text[..padding];
|
||||
vm.End = text[^padding..];
|
||||
}
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,13 @@ namespace BTCPayServer.Components.TruncateCenter
|
||||
public class TruncateCenterViewModel
|
||||
{
|
||||
public string Text { get; set; }
|
||||
public string Truncated { get; set; }
|
||||
public string Start { get; set; }
|
||||
public string End { get; set; }
|
||||
public string Classes { get; set; }
|
||||
public string Link { get; set; }
|
||||
public int Padding { get; set; }
|
||||
public bool Copy { get; set; }
|
||||
public bool Elastic { get; set; }
|
||||
public bool IsVue { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
<div class="d-sm-flex align-items-center justify-content-between">
|
||||
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId" class="unobtrusive-link">
|
||||
<h2 class="mb-1">@Model.Label</h2>
|
||||
<div class="text-muted fw-semibold">
|
||||
<div class="text-muted fw-semibold" data-sensitive>
|
||||
@Model.Balance @Model.Network.CryptoCode
|
||||
</div>
|
||||
</a>
|
||||
|
@ -245,7 +245,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
EmbeddedCSS = request.EmbeddedCSS?.Trim(),
|
||||
NotificationUrl = request.NotificationUrl?.Trim(),
|
||||
Tagline = request.Tagline?.Trim(),
|
||||
PerksTemplate = request.PerksTemplate is not null ? _appService.SerializeTemplate(_appService.Parse(request.PerksTemplate.Trim(), request.TargetCurrency!)) : null,
|
||||
PerksTemplate = request.PerksTemplate is not null ? AppService.SerializeTemplate(AppService.Parse(request.PerksTemplate.Trim())) : null,
|
||||
// If Disqus shortname is not null or empty we assume that Disqus should be enabled
|
||||
DisqusEnabled = !string.IsNullOrEmpty(request.DisqusShortname?.Trim()),
|
||||
DisqusShortname = request.DisqusShortname?.Trim(),
|
||||
@ -272,7 +272,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
ShowDiscount = request.ShowDiscount,
|
||||
EnableTips = request.EnableTips,
|
||||
Currency = request.Currency,
|
||||
Template = request.Template != null ? _appService.SerializeTemplate(_appService.Parse(request.Template, request.Currency)) : null,
|
||||
Template = request.Template != null ? AppService.SerializeTemplate(AppService.Parse(request.Template)) : null,
|
||||
ButtonText = request.FixedAmountPayButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
|
||||
CustomButtonText = request.CustomAmountPayButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
|
||||
CustomTipText = request.TipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
|
||||
@ -331,7 +331,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Currency = settings.Currency,
|
||||
Items = JsonConvert.DeserializeObject(
|
||||
JsonConvert.SerializeObject(
|
||||
_appService.Parse(settings.Template, settings.Currency),
|
||||
AppService.Parse(settings.Template),
|
||||
new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
|
||||
@ -363,8 +363,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
try
|
||||
{
|
||||
// Just checking if we can serialize, we don't care about the currency
|
||||
_appService.SerializeTemplate(_appService.Parse(request.Template, "USD"));
|
||||
// Just checking if we can serialize
|
||||
AppService.SerializeTemplate(AppService.Parse(request.Template));
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -406,7 +406,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Tagline = settings.Tagline,
|
||||
Perks = JsonConvert.DeserializeObject(
|
||||
JsonConvert.SerializeObject(
|
||||
_appService.Parse(settings.PerksTemplate, settings.TargetCurrency),
|
||||
AppService.Parse(settings.PerksTemplate),
|
||||
new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
|
||||
@ -453,8 +453,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
try
|
||||
{
|
||||
// Just checking if we can serialize, we don't care about the currency
|
||||
_appService.SerializeTemplate(_appService.Parse(request.PerksTemplate, "USD"));
|
||||
// Just checking if we can serialize
|
||||
AppService.SerializeTemplate(AppService.Parse(request.PerksTemplate));
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -183,7 +183,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Amount), "The amount should be 0 or more.");
|
||||
}
|
||||
request.Checkout = request.Checkout ?? new CreateInvoiceRequest.CheckoutOptions();
|
||||
request.Checkout ??= new CreateInvoiceRequest.CheckoutOptions();
|
||||
if (request.Checkout.PaymentMethods?.Any() is true)
|
||||
{
|
||||
for (int i = 0; i < request.Checkout.PaymentMethods.Length; i++)
|
||||
@ -226,7 +226,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var invoice = await _invoiceController.CreateInvoiceCoreRaw(request, store,
|
||||
@ -383,14 +383,15 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
if (invoicePaymentMethod is null)
|
||||
{
|
||||
this.ModelState.AddModelError(nameof(request.PaymentMethod), "Please select one of the payment methods which were available for the original invoice");
|
||||
ModelState.AddModelError(nameof(request.PaymentMethod), "Please select one of the payment methods which were available for the original invoice");
|
||||
}
|
||||
if (request.RefundVariant is null)
|
||||
this.ModelState.AddModelError(nameof(request.RefundVariant), "`refundVariant` is mandatory");
|
||||
ModelState.AddModelError(nameof(request.RefundVariant), "`refundVariant` is mandatory");
|
||||
if (!ModelState.IsValid || invoicePaymentMethod is null || paymentMethodId is null)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
var cryptoPaid = invoicePaymentMethod.Calculate().Paid.ToDecimal(MoneyUnit.BTC);
|
||||
var accounting = invoicePaymentMethod.Calculate();
|
||||
var cryptoPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC);
|
||||
var cdCurrency = _currencyNameTable.GetCurrencyData(invoice.Currency, true);
|
||||
var paidCurrency = Math.Round(cryptoPaid * invoicePaymentMethod.Rate, cdCurrency.Divisibility);
|
||||
var rateResult = await _rateProvider.FetchRate(
|
||||
@ -398,8 +399,10 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
store.GetStoreBlob().GetRateRules(_networkProvider),
|
||||
cancellationToken
|
||||
);
|
||||
var cryptoCode = invoicePaymentMethod.GetId().CryptoCode;
|
||||
var paymentMethodDivisibility = _currencyNameTable.GetCurrencyData(paymentMethodId.CryptoCode, false)?.Divisibility ?? 8;
|
||||
var createPullPayment = new HostedServices.CreatePullPayment()
|
||||
var paidAmount = cryptoPaid.RoundToSignificant(paymentMethodDivisibility);
|
||||
var createPullPayment = new CreatePullPayment
|
||||
{
|
||||
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration,
|
||||
Name = request.Name ?? $"Refund {invoice.Id}",
|
||||
@ -411,37 +414,61 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
if (request.RefundVariant != RefundVariant.Custom)
|
||||
{
|
||||
if (request.CustomAmount is not null)
|
||||
this.ModelState.AddModelError(nameof(request.CustomAmount), "CustomAmount should only be set if the refundVariant is Custom");
|
||||
ModelState.AddModelError(nameof(request.CustomAmount), "CustomAmount should only be set if the refundVariant is Custom");
|
||||
if (request.CustomCurrency is not null)
|
||||
this.ModelState.AddModelError(nameof(request.CustomCurrency), "CustomCurrency should only be set if the refundVariant is Custom");
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
ModelState.AddModelError(nameof(request.CustomCurrency), "CustomCurrency should only be set if the refundVariant is Custom");
|
||||
}
|
||||
if (request.SubtractPercentage is < 0 or > 100)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.SubtractPercentage), "Percentage must be a numeric value between 0 and 100");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
var appliedDivisibility = paymentMethodDivisibility;
|
||||
switch (request.RefundVariant)
|
||||
{
|
||||
case RefundVariant.RateThen:
|
||||
createPullPayment.Currency = invoicePaymentMethod.GetId().CryptoCode;
|
||||
createPullPayment.Amount = cryptoPaid.RoundToSignificant(paymentMethodDivisibility);
|
||||
createPullPayment.Currency = cryptoCode;
|
||||
createPullPayment.Amount = paidAmount;
|
||||
createPullPayment.AutoApproveClaims = true;
|
||||
break;
|
||||
|
||||
case RefundVariant.CurrentRate:
|
||||
createPullPayment.Currency = invoicePaymentMethod.GetId().CryptoCode;
|
||||
createPullPayment.Amount = Math.Round(paidCurrency / rateResult.BidAsk.Bid, paymentMethodDivisibility);
|
||||
createPullPayment.Currency = cryptoCode;
|
||||
createPullPayment.Amount = Math.Round(paidCurrency / rateResult.BidAsk.Bid, appliedDivisibility);
|
||||
createPullPayment.AutoApproveClaims = true;
|
||||
break;
|
||||
|
||||
case RefundVariant.Fiat:
|
||||
appliedDivisibility = cdCurrency.Divisibility;
|
||||
createPullPayment.Currency = invoice.Currency;
|
||||
createPullPayment.Amount = paidCurrency;
|
||||
createPullPayment.AutoApproveClaims = false;
|
||||
break;
|
||||
|
||||
case RefundVariant.OverpaidAmount:
|
||||
if (invoice.ExceptionStatus != InvoiceExceptionStatus.PaidOver)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.RefundVariant), "Invoice is not overpaid");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
var dueAmount = accounting.TotalDue.ToDecimal(MoneyUnit.BTC);
|
||||
createPullPayment.Currency = cryptoCode;
|
||||
createPullPayment.Amount = Math.Round(paidAmount - dueAmount, appliedDivisibility);
|
||||
createPullPayment.AutoApproveClaims = true;
|
||||
break;
|
||||
|
||||
case RefundVariant.Custom:
|
||||
if (request.CustomAmount is null || (request.CustomAmount is decimal v && v <= 0))
|
||||
{
|
||||
this.ModelState.AddModelError(nameof(request.CustomAmount), "Amount must be greater than 0");
|
||||
ModelState.AddModelError(nameof(request.CustomAmount), "Amount must be greater than 0");
|
||||
}
|
||||
|
||||
if (
|
||||
@ -472,6 +499,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
ModelState.AddModelError(nameof(request.RefundVariant), "Please select a valid refund option");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
// reduce by percentage
|
||||
if (request.SubtractPercentage is > 0 and <= 100)
|
||||
{
|
||||
var reduceByAmount = createPullPayment.Amount * (request.SubtractPercentage / 100);
|
||||
createPullPayment.Amount = Math.Round(createPullPayment.Amount - reduceByAmount, appliedDivisibility);
|
||||
}
|
||||
|
||||
var ppId = await _pullPaymentService.CreatePullPayment(createPullPayment);
|
||||
|
||||
|
@ -133,7 +133,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
"A valid node info was not provided to open a channel with");
|
||||
}
|
||||
|
||||
if (request.ChannelAmount == null)
|
||||
if (request?.ChannelAmount is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.ChannelAmount), "ChannelAmount is missing");
|
||||
}
|
||||
@ -142,7 +142,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
ModelState.AddModelError(nameof(request.ChannelAmount), "ChannelAmount must be more than 0");
|
||||
}
|
||||
|
||||
if (request.FeeRate == null)
|
||||
if (request?.FeeRate is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.FeeRate), "FeeRate is missing");
|
||||
}
|
||||
|
@ -352,7 +352,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> CreatePayoutThroughStore(string storeId, CreatePayoutThroughStoreRequest request)
|
||||
{
|
||||
if (request.Approved is true)
|
||||
if (request?.Approved is true)
|
||||
{
|
||||
if (!(await _authorizationService.AuthorizeAsync(User, null,
|
||||
new PolicyRequirement(Policies.CanCreatePullPayments))).Succeeded)
|
||||
|
@ -0,0 +1,42 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers.Greenfield;
|
||||
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldServerRolesController : ControllerBase
|
||||
{
|
||||
private readonly StoreRepository _storeRepository;
|
||||
|
||||
public GreenfieldServerRolesController(StoreRepository storeRepository)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/server/roles")]
|
||||
public async Task<IActionResult> GetServerRoles()
|
||||
{
|
||||
return Ok(FromModel(await _storeRepository.GetStoreRoles(null, false, false)));
|
||||
}
|
||||
private List<RoleData> FromModel(StoreRepository.StoreRole[] data)
|
||||
{
|
||||
return data.Select(r => new RoleData() {Role = r.Role, Id = r.Id, Permissions = r.Permissions, IsServerRole = true}).ToList();
|
||||
}
|
||||
|
||||
private IActionResult StoreNotFound()
|
||||
{
|
||||
return this.CreateAPIError(404, "store-not-found", "The store was not found");
|
||||
}
|
||||
}
|
4
BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedLightningPayoutProcessorsController.cs
4
BTCPayServer/Controllers/GreenField/GreenfieldStoreAutomatedLightningPayoutProcessorsController.cs
@ -2,6 +2,7 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
@ -69,6 +70,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
public async Task<IActionResult> UpdateStoreLightningAutomatedPayoutProcessor(
|
||||
string storeId, string paymentMethod, LightningAutomatedPayoutSettings request)
|
||||
{
|
||||
AutomatedPayoutConstants.ValidateInterval(ModelState, request.IntervalSeconds, nameof(request.IntervalSeconds));
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
paymentMethod = PaymentMethodId.Parse(paymentMethod).ToString();
|
||||
var activeProcessor =
|
||||
(await _payoutProcessorService.GetProcessors(
|
||||
|
@ -1,7 +1,9 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
@ -75,6 +77,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
public async Task<IActionResult> UpdateStoreOnchainAutomatedPayoutProcessor(
|
||||
string storeId, string paymentMethod, OnChainAutomatedPayoutSettings request)
|
||||
{
|
||||
AutomatedPayoutConstants.ValidateInterval(ModelState, request.IntervalSeconds, nameof(request.IntervalSeconds));
|
||||
if (request.FeeBlockTarget is int t && (t < 1 || t > 1000))
|
||||
ModelState.AddModelError(nameof(request.FeeBlockTarget), "The feeBlockTarget should be between 1 and 1000");
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
paymentMethod = PaymentMethodId.Parse(paymentMethod).ToString();
|
||||
var activeProcessor =
|
||||
(await _payoutProcessorService.GetProcessors(
|
||||
|
@ -585,6 +585,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
await _delayedTransactionBroadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2.0),
|
||||
transaction, network);
|
||||
_payjoinClient.MinimumFeeRate = minRelayFee;
|
||||
var payjoinPSBT = await _payjoinClient.RequestPayjoin(
|
||||
new BitcoinUrlBuilder(signingContext.PayJoinBIP21, network.NBitcoinNetwork),
|
||||
new PayjoinWallet(derivationScheme),
|
||||
|
@ -0,0 +1,48 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStoreRolesController : ControllerBase
|
||||
{
|
||||
private readonly StoreRepository _storeRepository;
|
||||
|
||||
public GreenfieldStoreRolesController(StoreRepository storeRepository)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/roles")]
|
||||
public async Task<IActionResult> GetStoreRoles(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
return store == null
|
||||
? StoreNotFound()
|
||||
: Ok(FromModel(await _storeRepository.GetStoreRoles(storeId, false, false)));
|
||||
}
|
||||
|
||||
|
||||
private List<RoleData> FromModel(StoreRepository.StoreRole[] data)
|
||||
{
|
||||
return data.Select(r => new RoleData() {Role = r.Role, Id = r.Id, Permissions = r.Permissions, IsServerRole = r.IsServerRole}).ToList();
|
||||
}
|
||||
|
||||
private IActionResult StoreNotFound()
|
||||
{
|
||||
return this.CreateAPIError(404, "store-not-found", "The store was not found");
|
||||
}
|
||||
}
|
||||
}
|
@ -63,8 +63,19 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
return StoreNotFound();
|
||||
}
|
||||
//we do not need to validate the role string as any value other than `StoreRoles.Owner` is currently treated like a guest
|
||||
if (await _storeRepository.AddStoreUser(storeId, request.UserId, request.Role))
|
||||
StoreRoleId roleId = null;
|
||||
|
||||
if (request.Role is not null)
|
||||
{
|
||||
roleId = await _storeRepository.ResolveStoreRoleId(storeId, request.Role);
|
||||
if (roleId is null)
|
||||
ModelState.AddModelError(nameof(request.Role), "The role id provided does not exist");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
if (await _storeRepository.AddStoreUser(storeId, request.UserId, roleId))
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
@ -74,7 +85,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
private IEnumerable<StoreUserData> FromModel(Data.StoreData data)
|
||||
{
|
||||
return data.UserStores.Select(store => new StoreUserData() { UserId = store.ApplicationUserId, Role = store.Role });
|
||||
return data.UserStores.Select(store => new StoreUserData() { UserId = store.ApplicationUserId, Role = store.StoreRoleId });
|
||||
}
|
||||
private IActionResult StoreNotFound()
|
||||
{
|
||||
|
@ -153,15 +153,24 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
var delivery = await StoreRepository.GetWebhookDelivery(CurrentStoreId, webhookId, deliveryId);
|
||||
if (delivery is null)
|
||||
return WebhookDeliveryNotFound();
|
||||
if (delivery.GetBlob().IsPruned())
|
||||
return WebhookDeliveryPruned();
|
||||
return this.Ok(new JValue(await WebhookSender.Redeliver(deliveryId)));
|
||||
}
|
||||
|
||||
private IActionResult WebhookDeliveryPruned()
|
||||
{
|
||||
return this.CreateAPIError(409, "webhookdelivery-pruned", "This webhook delivery has been pruned, so it can't be redelivered");
|
||||
}
|
||||
|
||||
[HttpGet("~/api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request")]
|
||||
public async Task<IActionResult> GetDeliveryRequest(string storeId, string webhookId, string deliveryId)
|
||||
{
|
||||
var delivery = await StoreRepository.GetWebhookDelivery(CurrentStoreId, webhookId, deliveryId);
|
||||
if (delivery is null)
|
||||
return WebhookDeliveryNotFound();
|
||||
if (delivery.GetBlob().IsPruned())
|
||||
return WebhookDeliveryPruned();
|
||||
return File(delivery.GetBlob().Request, "application/json");
|
||||
}
|
||||
|
||||
|
@ -115,11 +115,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
internal static Client.Models.StoreData FromModel(Data.StoreData data)
|
||||
{
|
||||
var storeBlob = data.GetStoreBlob();
|
||||
return new Client.Models.StoreData()
|
||||
return new Client.Models.StoreData
|
||||
{
|
||||
Id = data.Id,
|
||||
Name = data.StoreName,
|
||||
Website = data.StoreWebsite,
|
||||
SupportUrl = storeBlob.StoreSupportUrl,
|
||||
SpeedPolicy = data.SpeedPolicy,
|
||||
DefaultPaymentMethod = data.GetDefaultPaymentId()?.ToStringNormalized(),
|
||||
//blob
|
||||
@ -163,7 +164,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
var blob = model.GetStoreBlob();
|
||||
model.StoreName = restModel.Name;
|
||||
model.StoreName = restModel.Name;
|
||||
model.StoreWebsite = restModel.Website;
|
||||
model.SpeedPolicy = restModel.SpeedPolicy;
|
||||
model.SetDefaultPaymentId(defaultPaymentMethod);
|
||||
@ -186,6 +186,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
blob.ShowRecommendedFee = restModel.ShowRecommendedFee;
|
||||
blob.RecommendedFeeBlockTarget = restModel.RecommendedFeeBlockTarget;
|
||||
blob.DefaultLang = restModel.DefaultLang;
|
||||
blob.StoreSupportUrl = restModel.SupportUrl;
|
||||
blob.MonitoringExpiration = restModel.MonitoringExpiration;
|
||||
blob.InvoiceExpiration = restModel.InvoiceExpiration;
|
||||
blob.DisplayExpirationTimer = restModel.DisplayExpirationTimer;
|
||||
@ -238,7 +239,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
ModelState.AddModelError(nameof(request.DisplayExpirationTimer), "DisplayExpirationTimer can only be between 1 and 34560 mins");
|
||||
if (request.MonitoringExpiration < TimeSpan.FromMinutes(10) && request.MonitoringExpiration > TimeSpan.FromMinutes(60 * 24 * 24))
|
||||
ModelState.AddModelError(nameof(request.MonitoringExpiration), "MonitoringExpiration can only be between 10 and 34560 mins");
|
||||
if (request.PaymentTolerance < 0 && request.PaymentTolerance > 100)
|
||||
if (request.PaymentTolerance < 0 || request.PaymentTolerance > 100)
|
||||
ModelState.AddModelError(nameof(request.PaymentTolerance), "PaymentTolerance can only be between 0 and 100 percent");
|
||||
|
||||
if (request.PaymentMethodCriteria?.Any() is true)
|
||||
|
@ -1319,5 +1319,27 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
return GetFromActionResult<CrowdfundAppData>(await GetController<GreenfieldAppsController>().GetCrowdfundApp(appId));
|
||||
}
|
||||
public override async Task<PullPaymentData> RefundInvoice(string storeId, string invoiceId, RefundInvoiceRequest request, CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<PullPaymentData>(await GetController<GreenfieldInvoiceController>().RefundInvoice(storeId, invoiceId, request, token));
|
||||
}
|
||||
public override async Task RevokeAPIKey(string userId, string apikey, CancellationToken token = default)
|
||||
{
|
||||
HandleActionResult(await GetController<GreenfieldApiKeysController>().RevokeAPIKey(userId, apikey));
|
||||
}
|
||||
|
||||
public override async Task<ApiKeyData> CreateAPIKey(string userId, CreateApiKeyRequest request, CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<ApiKeyData>(await GetController<GreenfieldApiKeysController>().CreateUserAPIKey(userId, request));
|
||||
}
|
||||
|
||||
public override async Task<List<RoleData>> GetServerRoles(CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<List<RoleData>>(await GetController<GreenfieldServerRolesController>().GetServerRoles());
|
||||
}
|
||||
public override async Task<List<RoleData>> GetStoreRoles(string storeId, CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<List<RoleData>>(await GetController<GreenfieldStoreRolesController>().GetStoreRoles(storeId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
@ -9,30 +8,18 @@ using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Components.StoreSelector;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using ExchangeSharp;
|
||||
using Google.Apis.Auth.OAuth2;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Payment;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
@ -92,23 +79,14 @@ namespace BTCPayServer.Controllers
|
||||
var store = await _storeRepository.FindStore(storeId, userId);
|
||||
if (store != null)
|
||||
{
|
||||
return RedirectToStore(store);
|
||||
return RedirectToStore(userId, store);
|
||||
}
|
||||
}
|
||||
|
||||
var stores = await _storeRepository.GetStoresByUserId(userId);
|
||||
if (stores.Any())
|
||||
{
|
||||
// redirect to first store
|
||||
return RedirectToStore(stores.First());
|
||||
}
|
||||
|
||||
var vm = new HomeViewModel
|
||||
{
|
||||
HasStore = stores.Any()
|
||||
};
|
||||
|
||||
return View("Home", vm);
|
||||
return stores.Any()
|
||||
? RedirectToStore(userId, stores.First())
|
||||
: RedirectToAction(nameof(UIUserStoresController.CreateStore), "UIUserStores");
|
||||
}
|
||||
|
||||
return Challenge();
|
||||
@ -220,9 +198,9 @@ namespace BTCPayServer.Controllers
|
||||
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
|
||||
}
|
||||
|
||||
public RedirectToActionResult RedirectToStore(StoreData store)
|
||||
public RedirectToActionResult RedirectToStore(string userId, StoreData store)
|
||||
{
|
||||
return store.HasPermission(Policies.CanModifyStoreSettings)
|
||||
return store.HasPermission(userId, Policies.CanModifyStoreSettings)
|
||||
? RedirectToAction("Dashboard", "UIStores", new { storeId = store.Id })
|
||||
: RedirectToAction("ListInvoices", "UIInvoice", new { storeId = store.Id });
|
||||
}
|
||||
|
@ -347,23 +347,39 @@ namespace BTCPayServer.Controllers
|
||||
RateRules rules;
|
||||
RateResult rateResult;
|
||||
CreatePullPayment createPullPayment;
|
||||
PaymentMethodAccounting accounting;
|
||||
var pms = invoice.GetPaymentMethods();
|
||||
var paymentMethod = pms.SingleOrDefault(method => method.GetId() == paymentMethodId);
|
||||
var appliedDivisibility = paymentMethodDivisibility;
|
||||
decimal dueAmount = default;
|
||||
decimal paidAmount = default;
|
||||
decimal cryptoPaid = default;
|
||||
|
||||
//TODO: Make this clean
|
||||
if (paymentMethod is null && paymentMethodId.PaymentType == LightningPaymentType.Instance)
|
||||
{
|
||||
paymentMethod = pms[new PaymentMethodId(paymentMethodId.CryptoCode, PaymentTypes.LNURLPay)];
|
||||
}
|
||||
|
||||
if (paymentMethod != null)
|
||||
{
|
||||
accounting = paymentMethod.Calculate();
|
||||
cryptoPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC);
|
||||
dueAmount = accounting.TotalDue.ToDecimal(MoneyUnit.BTC);
|
||||
paidAmount = cryptoPaid.RoundToSignificant(appliedDivisibility);
|
||||
}
|
||||
|
||||
var isPaidOver = invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver;
|
||||
decimal? overpaidAmount = isPaidOver ? Math.Round(paidAmount - dueAmount, appliedDivisibility) : null;
|
||||
|
||||
switch (model.RefundStep)
|
||||
{
|
||||
case RefundSteps.SelectPaymentMethod:
|
||||
model.RefundStep = RefundSteps.SelectRate;
|
||||
model.Title = "How much to refund?";
|
||||
var pms = invoice.GetPaymentMethods();
|
||||
var paymentMethod = pms.SingleOrDefault(method => method.GetId() == paymentMethodId);
|
||||
|
||||
//TODO: Make this clean
|
||||
if (paymentMethod is null && paymentMethodId.PaymentType == LightningPaymentType.Instance)
|
||||
if (paymentMethod != null && cryptoPaid != default)
|
||||
{
|
||||
paymentMethod = pms[new PaymentMethodId(paymentMethodId.CryptoCode, PaymentTypes.LNURLPay)];
|
||||
}
|
||||
|
||||
if (paymentMethod != null)
|
||||
{
|
||||
var cryptoPaid = paymentMethod.Calculate().Paid.ToDecimal(MoneyUnit.BTC);
|
||||
var paidCurrency = Math.Round(cryptoPaid * paymentMethod.Rate, cdCurrency.Divisibility);
|
||||
model.CryptoAmountThen = cryptoPaid.RoundToSignificant(paymentMethodDivisibility);
|
||||
model.RateThenText = _displayFormatter.Currency(model.CryptoAmountThen, paymentMethodId.CryptoCode);
|
||||
@ -383,8 +399,15 @@ namespace BTCPayServer.Controllers
|
||||
model.CurrentRateText = _displayFormatter.Currency(model.CryptoAmountNow, paymentMethodId.CryptoCode);
|
||||
model.FiatAmount = paidCurrency;
|
||||
}
|
||||
model.CryptoCode = paymentMethodId.CryptoCode;
|
||||
model.CryptoDivisibility = paymentMethodDivisibility;
|
||||
model.InvoiceDivisibility = cdCurrency.Divisibility;
|
||||
model.InvoiceCurrency = invoice.Currency;
|
||||
model.CustomAmount = model.FiatAmount;
|
||||
model.CustomCurrency = invoice.Currency;
|
||||
model.SubtractPercentage = 0;
|
||||
model.OverpaidAmount = overpaidAmount;
|
||||
model.OverpaidAmountText = overpaidAmount != null ? _displayFormatter.Currency(overpaidAmount.Value, paymentMethodId.CryptoCode) : null;
|
||||
model.FiatText = _displayFormatter.Currency(model.FiatAmount, invoice.Currency);
|
||||
return View("_RefundModal", model);
|
||||
|
||||
@ -399,6 +422,15 @@ namespace BTCPayServer.Controllers
|
||||
var authorizedForAutoApprove = (await
|
||||
_authorizationService.AuthorizeAsync(User, invoice.StoreId, Policies.CanCreatePullPayments))
|
||||
.Succeeded;
|
||||
if (model.SubtractPercentage is < 0 or > 100)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SubtractPercentage), "Percentage must be a numeric value between 0 and 100");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View("_RefundModal", model);
|
||||
}
|
||||
|
||||
switch (model.SelectedRefundOption)
|
||||
{
|
||||
case "RateThen":
|
||||
@ -414,27 +446,47 @@ namespace BTCPayServer.Controllers
|
||||
break;
|
||||
|
||||
case "Fiat":
|
||||
appliedDivisibility = cdCurrency.Divisibility;
|
||||
createPullPayment.Currency = invoice.Currency;
|
||||
createPullPayment.Amount = model.FiatAmount;
|
||||
createPullPayment.AutoApproveClaims = false;
|
||||
break;
|
||||
|
||||
case "OverpaidAmount":
|
||||
model.Title = "How much to refund?";
|
||||
model.RefundStep = RefundSteps.SelectRate;
|
||||
|
||||
if (isPaidOver)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption), "Invoice is not overpaid");
|
||||
}
|
||||
if (overpaidAmount == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption), "Overpaid amount cannot be calculated");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
createPullPayment.Currency = paymentMethodId.CryptoCode;
|
||||
createPullPayment.Amount = overpaidAmount!.Value;
|
||||
createPullPayment.AutoApproveClaims = true;
|
||||
break;
|
||||
|
||||
case "Custom":
|
||||
model.Title = "How much to refund?";
|
||||
|
||||
model.RefundStep = RefundSteps.SelectRate;
|
||||
|
||||
if (model.CustomAmount <= 0)
|
||||
{
|
||||
model.AddModelError(refundModel => refundModel.CustomAmount, "Amount must be greater than 0", this);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(model.CustomCurrency) ||
|
||||
_CurrencyNameTable.GetCurrencyData(model.CustomCurrency, false) == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.CustomCurrency), "Invalid currency");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View("_RefundModal", model);
|
||||
@ -468,6 +520,13 @@ namespace BTCPayServer.Controllers
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
// reduce by percentage
|
||||
if (model.SubtractPercentage is > 0 and <= 100)
|
||||
{
|
||||
var reduceByAmount = createPullPayment.Amount * (model.SubtractPercentage / 100);
|
||||
createPullPayment.Amount = Math.Round(createPullPayment.Amount - reduceByAmount, appliedDivisibility);
|
||||
}
|
||||
|
||||
var ppId = await _paymentHostedService.CreatePullPayment(createPullPayment);
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
@ -579,7 +638,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
if (explorer is null)
|
||||
return NotSupported("This feature is only available to BTC wallets");
|
||||
if (this.GetCurrentStore().Role != StoreRoles.Owner)
|
||||
if (!GetCurrentStore().HasPermission(GetUserId(), Policies.CanModifyStoreSettings))
|
||||
return Forbid();
|
||||
|
||||
var derivationScheme = (this.GetCurrentStore().GetDerivationSchemeSettings(_NetworkProvider, network.CryptoCode))?.AccountDerivation;
|
||||
@ -795,16 +854,23 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var isAltcoinsBuild = false;
|
||||
#if ALTCOINS
|
||||
isAltcoinsBuild = true;
|
||||
isAltcoinsBuild = true;
|
||||
#endif
|
||||
|
||||
var orderId = invoice.Metadata.OrderId;
|
||||
var supportUrl = !string.IsNullOrEmpty(storeBlob.StoreSupportUrl)
|
||||
? storeBlob.StoreSupportUrl
|
||||
.Replace("{OrderId}", string.IsNullOrEmpty(orderId) ? string.Empty : Uri.EscapeDataString(orderId))
|
||||
.Replace("{InvoiceId}", Uri.EscapeDataString(invoice.Id))
|
||||
: null;
|
||||
|
||||
var model = new PaymentModel
|
||||
{
|
||||
Activated = paymentMethodDetails.Activated,
|
||||
CryptoCode = network.CryptoCode,
|
||||
RootPath = Request.PathBase.Value.WithTrailingSlash(),
|
||||
OrderId = invoice.Metadata.OrderId,
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = orderId,
|
||||
InvoiceId = invoiceId,
|
||||
DefaultLang = lang ?? invoice.DefaultLanguage ?? storeBlob.DefaultLang ?? "en",
|
||||
ShowPayInWalletButton = storeBlob.ShowPayInWalletButton,
|
||||
ShowStoreHeader = storeBlob.ShowStoreHeader,
|
||||
@ -836,6 +902,7 @@ namespace BTCPayServer.Controllers
|
||||
ReceiptLink = receiptUrl,
|
||||
RedirectAutomatically = invoice.RedirectAutomatically,
|
||||
StoreName = store.StoreName,
|
||||
StoreSupportUrl = supportUrl,
|
||||
TxCount = accounting.TxRequired,
|
||||
TxCountForFee = storeBlob.NetworkFeeMode switch
|
||||
{
|
||||
@ -1006,34 +1073,44 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> ListInvoices(InvoicesModel? model = null)
|
||||
{
|
||||
model = this.ParseListQuery(model ?? new InvoicesModel());
|
||||
var fs = new SearchString(model.SearchTerm);
|
||||
var timezoneOffset = model.TimezoneOffset ?? 0;
|
||||
var searchTerm = string.IsNullOrEmpty(model.SearchText) ? model.SearchTerm : $"{model.SearchText},{model.SearchTerm}";
|
||||
var fs = new SearchString(searchTerm, timezoneOffset);
|
||||
string? storeId = model.StoreId;
|
||||
var storeIds = new HashSet<string>();
|
||||
if (fs.GetFilterArray("storeid") is string[] l)
|
||||
if (storeId is not null)
|
||||
{
|
||||
storeIds.Add(storeId);
|
||||
}
|
||||
if (fs.GetFilterArray("storeid") is { } l)
|
||||
{
|
||||
foreach (var i in l)
|
||||
storeIds.Add(i);
|
||||
}
|
||||
if (storeId is not null)
|
||||
{
|
||||
storeIds.Add(storeId);
|
||||
model.StoreId = storeId;
|
||||
}
|
||||
model.StoreIds = storeIds.ToArray();
|
||||
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(model.SearchTerm, model.TimezoneOffset ?? 0);
|
||||
invoiceQuery.StoreId = model.StoreIds;
|
||||
model.Search = fs;
|
||||
model.SearchText = fs.TextSearch;
|
||||
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(fs, timezoneOffset);
|
||||
invoiceQuery.StoreId = storeIds.ToArray();
|
||||
invoiceQuery.Take = model.Count;
|
||||
invoiceQuery.Skip = model.Skip;
|
||||
invoiceQuery.IncludeRefunds = true;
|
||||
var list = await _InvoiceRepository.GetInvoices(invoiceQuery);
|
||||
|
||||
model.IncludeArchived = invoiceQuery.IncludeArchived;
|
||||
// Apps
|
||||
var apps = await _appService.GetAllApps(GetUserId(), false, storeId);
|
||||
model.Apps = apps.Select(a => new InvoiceAppModel
|
||||
{
|
||||
Id = a.Id,
|
||||
AppName = a.AppName,
|
||||
AppType = a.AppType,
|
||||
AppOrderId = AppService.GetAppOrderId(a.AppType, a.Id)
|
||||
}).ToList();
|
||||
|
||||
foreach (var invoice in list)
|
||||
{
|
||||
var state = invoice.GetInvoiceState();
|
||||
model.Invoices.Add(new InvoiceModel()
|
||||
model.Invoices.Add(new InvoiceModel
|
||||
{
|
||||
Status = state,
|
||||
ShowCheckout = invoice.Status == InvoiceStatusLegacy.New,
|
||||
@ -1052,10 +1129,9 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private InvoiceQuery GetInvoiceQuery(string? searchTerm = null, int timezoneOffset = 0)
|
||||
private InvoiceQuery GetInvoiceQuery(SearchString fs, int timezoneOffset = 0)
|
||||
{
|
||||
var fs = new SearchString(searchTerm);
|
||||
var invoiceQuery = new InvoiceQuery()
|
||||
return new InvoiceQuery
|
||||
{
|
||||
TextSearch = fs.TextSearch,
|
||||
UserId = GetUserId(),
|
||||
@ -1069,7 +1145,6 @@ namespace BTCPayServer.Controllers
|
||||
StartDate = fs.GetFilterDate("startdate", timezoneOffset),
|
||||
EndDate = fs.GetFilterDate("enddate", timezoneOffset)
|
||||
};
|
||||
return invoiceQuery;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -1080,17 +1155,17 @@ namespace BTCPayServer.Controllers
|
||||
var model = new InvoiceExport(_CurrencyNameTable);
|
||||
var fs = new SearchString(searchTerm);
|
||||
var storeIds = new HashSet<string>();
|
||||
if (fs.GetFilterArray("storeid") is string[] l)
|
||||
{
|
||||
foreach (var i in l)
|
||||
storeIds.Add(i);
|
||||
}
|
||||
if (storeId is not null)
|
||||
{
|
||||
storeIds.Add(storeId);
|
||||
}
|
||||
if (fs.GetFilterArray("storeid") is { } l)
|
||||
{
|
||||
foreach (var i in l)
|
||||
storeIds.Add(i);
|
||||
}
|
||||
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm, timezoneOffset);
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(fs, timezoneOffset);
|
||||
invoiceQuery.StoreId = storeIds.ToArray();
|
||||
invoiceQuery.Skip = 0;
|
||||
invoiceQuery.Take = int.MaxValue;
|
||||
@ -1301,10 +1376,15 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
case JTokenType.Array:
|
||||
var items = item.Value.AsEnumerable().ToList();
|
||||
var arrayResult = new List<object>();
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
result.TryAdd($"{item.Key}[{i}]", ParsePosData(items[i]));
|
||||
arrayResult.Add(items[i] is JObject
|
||||
? ParsePosData(items[i])
|
||||
: items[i].ToString());
|
||||
}
|
||||
|
||||
result.TryAdd(item.Key, arrayResult);
|
||||
|
||||
break;
|
||||
case JTokenType.Object:
|
||||
|
@ -58,6 +58,7 @@ namespace BTCPayServer.Controllers
|
||||
private readonly InvoiceActivator _invoiceActivator;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly AppService _appService;
|
||||
|
||||
public WebhookSender WebhookNotificationManager { get; }
|
||||
|
||||
@ -81,6 +82,7 @@ namespace BTCPayServer.Controllers
|
||||
UIWalletsController walletsController,
|
||||
InvoiceActivator invoiceActivator,
|
||||
LinkGenerator linkGenerator,
|
||||
AppService appService,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
_displayFormatter = displayFormatter;
|
||||
@ -102,6 +104,7 @@ namespace BTCPayServer.Controllers
|
||||
_invoiceActivator = invoiceActivator;
|
||||
_linkGenerator = linkGenerator;
|
||||
_authorizationService = authorizationService;
|
||||
_appService = appService;
|
||||
}
|
||||
|
||||
|
||||
@ -154,6 +157,7 @@ namespace BTCPayServer.Controllers
|
||||
entity.Type = InvoiceType.TopUp;
|
||||
}
|
||||
|
||||
entity.StoreSupportUrl = storeBlob.StoreSupportUrl;
|
||||
entity.RedirectURLTemplate = invoice.RedirectURL ?? store.StoreWebsite;
|
||||
entity.RedirectAutomatically =
|
||||
invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically);
|
||||
@ -245,7 +249,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
entity.SpeedPolicy = invoice.Checkout.SpeedPolicy ?? store.SpeedPolicy;
|
||||
entity.DefaultLanguage = invoice.Checkout.DefaultLanguage;
|
||||
entity.DefaultPaymentMethod = invoice.Checkout.DefaultPaymentMethod;
|
||||
entity.DefaultPaymentMethod = invoice.Checkout.DefaultPaymentMethod ?? store.GetDefaultPaymentId()?.ToStringNormalized() ?? new PaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode, PaymentTypes.BTCLike).ToStringNormalized();
|
||||
entity.RedirectAutomatically = invoice.Checkout.RedirectAutomatically ?? storeBlob.RedirectAutomatically;
|
||||
entity.CheckoutType = invoice.Checkout.CheckoutType;
|
||||
entity.RequiresRefundEmail = invoice.Checkout.RequiresRefundEmail;
|
||||
|
@ -257,12 +257,12 @@ namespace BTCPayServer
|
||||
case CrowdfundAppType.AppType:
|
||||
var cfS = app.GetSettings<CrowdfundSettings>();
|
||||
currencyCode = cfS.TargetCurrency;
|
||||
items = _appService.Parse(cfS.PerksTemplate, cfS.TargetCurrency);
|
||||
items = AppService.Parse(cfS.PerksTemplate);
|
||||
break;
|
||||
case PointOfSaleAppType.AppType:
|
||||
posS = app.GetSettings<PointOfSaleSettings>();
|
||||
currencyCode = posS.Currency;
|
||||
items = _appService.Parse(posS.Template, posS.Currency);
|
||||
items = AppService.Parse(posS.Template);
|
||||
break;
|
||||
default:
|
||||
//TODO: Allow other apps to define lnurl support
|
||||
@ -361,11 +361,6 @@ namespace BTCPayServer
|
||||
|
||||
public ConcurrentDictionary<string, LightningAddressItem> Items { get; } = new();
|
||||
public ConcurrentDictionary<string, string[]> StoreToItemMap { get; } = new();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("~/.well-known/lnurlp/{username}")]
|
||||
|
@ -78,7 +78,7 @@ namespace BTCPayServer.Controllers
|
||||
model = this.ParseListQuery(model ?? new ListPaymentRequestsViewModel());
|
||||
|
||||
var store = GetCurrentStore();
|
||||
var includeArchived = new SearchString(model.SearchTerm).GetFilterBool("includearchived") == true;
|
||||
var includeArchived = new SearchString(model.SearchTerm, model.TimezoneOffset ?? 0).GetFilterBool("includearchived") == true;
|
||||
var result = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery
|
||||
{
|
||||
UserId = GetUserId(),
|
||||
|
192
BTCPayServer/Controllers/UIServerController.Roles.cs
Normal file
192
BTCPayServer/Controllers/UIServerController.Roles.cs
Normal file
@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class UIServerController
|
||||
{
|
||||
[Route("server/roles")]
|
||||
public async Task<IActionResult> ListRoles(
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
RolesViewModel model,
|
||||
string sortOrder = null
|
||||
)
|
||||
{
|
||||
model ??= new RolesViewModel();
|
||||
|
||||
model.DefaultRole = (await storeRepository.GetDefaultRole()).Role;
|
||||
var roles = await storeRepository.GetStoreRoles(null);
|
||||
|
||||
if (sortOrder != null)
|
||||
{
|
||||
switch (sortOrder)
|
||||
{
|
||||
case "desc":
|
||||
ViewData["NextRoleSortOrder"] = "asc";
|
||||
roles = roles.OrderByDescending(user => user.Role).ToArray();
|
||||
break;
|
||||
case "asc":
|
||||
roles = roles.OrderBy(user => user.Role).ToArray();
|
||||
ViewData["NextRoleSortOrder"] = "desc";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
model.Roles = roles.Skip(model.Skip).Take(model.Count).ToList();
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet("server/roles/{role}")]
|
||||
public async Task<IActionResult> CreateOrEditRole(
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
{
|
||||
if (role == "create")
|
||||
{
|
||||
ModelState.Remove(nameof(role));
|
||||
return View(new UpdateRoleViewModel());
|
||||
}
|
||||
else
|
||||
{
|
||||
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(role));
|
||||
if (roleData == null)
|
||||
return NotFound();
|
||||
return View(new UpdateRoleViewModel()
|
||||
{
|
||||
Policies = roleData.Permissions,
|
||||
Role = roleData.Role
|
||||
});
|
||||
}
|
||||
}
|
||||
[HttpPost("server/roles/{role}")]
|
||||
public async Task<IActionResult> CreateOrEditRole(
|
||||
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
[FromRoute] string role, UpdateRoleViewModel viewModel)
|
||||
{
|
||||
string successMessage = null;
|
||||
if (role == "create")
|
||||
{
|
||||
successMessage = "Role created";
|
||||
role = viewModel.Role;
|
||||
}
|
||||
else
|
||||
{
|
||||
successMessage = "Role updated";
|
||||
var storeRole = await storeRepository.GetStoreRole(new StoreRoleId(role));
|
||||
if (storeRole == null)
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
var r = await storeRepository.AddOrUpdateStoreRole(new StoreRoleId(role), viewModel.Policies);
|
||||
if (r is null)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Role could not be updated"
|
||||
});
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = successMessage
|
||||
});
|
||||
|
||||
return RedirectToAction(nameof(ListRoles));
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpGet("server/roles/{role}/delete")]
|
||||
public async Task<IActionResult> DeleteRole(
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
{
|
||||
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(role), true);
|
||||
if (roleData == null)
|
||||
return NotFound();
|
||||
|
||||
return View("Confirm",
|
||||
roleData.IsUsed is true
|
||||
? new ConfirmModel("Delete role",
|
||||
$"Unable to proceed: The role <strong>{Html.Encode(roleData.Role)}</strong> is currently assigned to one or more users, it cannot be removed.")
|
||||
: new ConfirmModel("Delete role",
|
||||
$"The role <strong>{Html.Encode(roleData.Role)}</strong> will be permanently deleted. Are you sure?",
|
||||
"Delete"));
|
||||
}
|
||||
|
||||
[HttpPost("server/roles/{role}/delete")]
|
||||
public async Task<IActionResult> DeleteRolePost(
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
{
|
||||
var roleId = new StoreRoleId(role);
|
||||
var roleData = await storeRepository.GetStoreRole(roleId, true);
|
||||
if (roleData == null)
|
||||
return NotFound();
|
||||
if (roleData.IsUsed is true)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var errorMessage = await storeRepository.RemoveStoreRole(roleId);
|
||||
if (errorMessage is null)
|
||||
{
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Role deleted";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = errorMessage;
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(ListRoles));
|
||||
}
|
||||
|
||||
[HttpGet("server/roles/{role}/default")]
|
||||
public async Task<IActionResult> SetDefaultRole(
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
{
|
||||
var resolved = await storeRepository.ResolveStoreRoleId(null, role);
|
||||
if (resolved is null)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Role could not be set as default";
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
await storeRepository.SetDefaultRole(role);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Role set default";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(ListRoles));
|
||||
}
|
||||
}
|
||||
}
|
||||
public class UpdateRoleViewModel
|
||||
{
|
||||
[Required]
|
||||
[Display(Name = "Role")]
|
||||
public string Role { get; set; }
|
||||
|
||||
[Display(Name = "Policies")] public List<string> Policies { get; set; } = new();
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@ -250,7 +251,7 @@ namespace BTCPayServer.Controllers
|
||||
CryptoCode = cryptoCode,
|
||||
Method = method,
|
||||
SetupRequest = request,
|
||||
Confirmation = string.IsNullOrEmpty(request.ExistingMnemonic),
|
||||
Confirmation = !isImport,
|
||||
Network = network,
|
||||
Source = isImport ? "SeedImported" : "NBXplorerGenerated",
|
||||
IsHotWallet = isImport ? request.SavePrivateKeys : method == WalletSetupMethod.HotWallet,
|
||||
@ -311,7 +312,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var result = await UpdateWallet(vm);
|
||||
|
||||
if (!ModelState.IsValid || !(result is RedirectToActionResult))
|
||||
if (!ModelState.IsValid || result is not RedirectToActionResult)
|
||||
return result;
|
||||
|
||||
if (!isImport)
|
||||
|
164
BTCPayServer/Controllers/UIStoresController.Roles.cs
Normal file
164
BTCPayServer/Controllers/UIStoresController.Roles.cs
Normal file
@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Amazon.S3.Transfer;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class UIStoresController
|
||||
{
|
||||
[Route("{storeId}/roles")]
|
||||
public async Task<IActionResult> ListRoles(
|
||||
string storeId,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
RolesViewModel model,
|
||||
string sortOrder = null
|
||||
)
|
||||
{
|
||||
model ??= new RolesViewModel();
|
||||
|
||||
model.DefaultRole = (await storeRepository.GetDefaultRole()).Role;
|
||||
var roles = await storeRepository.GetStoreRoles(storeId, false, false);
|
||||
|
||||
if (sortOrder != null)
|
||||
{
|
||||
switch (sortOrder)
|
||||
{
|
||||
case "desc":
|
||||
ViewData["NextRoleSortOrder"] = "asc";
|
||||
roles = roles.OrderByDescending(user => user.Role).ToArray();
|
||||
break;
|
||||
case "asc":
|
||||
roles = roles.OrderBy(user => user.Role).ToArray();
|
||||
ViewData["NextRoleSortOrder"] = "desc";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
model.Roles = roles.Skip(model.Skip).Take(model.Count).ToList();
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/roles/{role}")]
|
||||
public async Task<IActionResult> CreateOrEditRole(
|
||||
string storeId,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
{
|
||||
if (role == "create")
|
||||
{
|
||||
ModelState.Remove(nameof(role));
|
||||
return View(new UpdateRoleViewModel());
|
||||
}
|
||||
else
|
||||
{
|
||||
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(storeId, role));
|
||||
if (roleData == null)
|
||||
return NotFound();
|
||||
return View(new UpdateRoleViewModel()
|
||||
{
|
||||
Policies = roleData.Permissions,
|
||||
Role = roleData.Role
|
||||
});
|
||||
}
|
||||
}
|
||||
[HttpPost("{storeId}/roles/{role}")]
|
||||
public async Task<IActionResult> CreateOrEditRole(
|
||||
string storeId,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
[FromRoute] string role, UpdateRoleViewModel viewModel)
|
||||
{
|
||||
string successMessage = null;
|
||||
StoreRoleId roleId;
|
||||
if (role == "create")
|
||||
{
|
||||
successMessage = "Role created";
|
||||
role = viewModel.Role;
|
||||
roleId = new StoreRoleId(storeId, role);
|
||||
}
|
||||
else
|
||||
{
|
||||
successMessage = "Role updated";
|
||||
roleId = new StoreRoleId(storeId, role);
|
||||
var storeRole = await storeRepository.GetStoreRole(roleId);
|
||||
if (storeRole == null)
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
var r = await storeRepository.AddOrUpdateStoreRole(roleId, viewModel.Policies);
|
||||
if (r is null)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Role could not be updated"
|
||||
});
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = successMessage
|
||||
});
|
||||
|
||||
return RedirectToAction(nameof(ListRoles), new { storeId });
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpGet("{storeId}/roles/{role}/delete")]
|
||||
public async Task<IActionResult> DeleteRole(
|
||||
string storeId,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
{
|
||||
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(storeId, role), true);;
|
||||
if (roleData == null)
|
||||
return NotFound();
|
||||
|
||||
return View("Confirm",
|
||||
roleData.IsUsed is true
|
||||
? new ConfirmModel("Delete role",
|
||||
$"Unable to proceed: The role <strong>{Html.Encode(roleData.Role)}</strong> is currently assigned to one or more users, it cannot be removed.")
|
||||
: new ConfirmModel("Delete role",
|
||||
$"The role <strong>{Html.Encode(roleData.Role)}</strong> will be permanently deleted. Are you sure?",
|
||||
"Delete"));
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/roles/{role}/delete")]
|
||||
public async Task<IActionResult> DeleteRolePost(
|
||||
string storeId,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
{
|
||||
var roleId = new StoreRoleId(storeId, role);
|
||||
var roleData = await storeRepository.GetStoreRole(roleId, true);
|
||||
if (roleData == null)
|
||||
return NotFound();
|
||||
if (roleData.IsUsed is true)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
await storeRepository.RemoveStoreRole(roleId);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Role deleted";
|
||||
return RedirectToAction(nameof(ListRoles), new { storeId });
|
||||
}
|
||||
}
|
||||
}
|
@ -130,6 +130,7 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> StoreUsers()
|
||||
{
|
||||
StoreUsersViewModel vm = new StoreUsersViewModel();
|
||||
vm.Role = StoreRoleId.Guest.Role;
|
||||
await FillUsers(vm);
|
||||
return View(vm);
|
||||
}
|
||||
@ -142,7 +143,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
Email = u.Email,
|
||||
Id = u.Id,
|
||||
Role = u.Role
|
||||
Role = u.StoreRole.Role
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
@ -150,7 +151,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/users")]
|
||||
public async Task<IActionResult> StoreUsers(StoreUsersViewModel vm)
|
||||
public async Task<IActionResult> StoreUsers(string storeId, StoreUsersViewModel vm)
|
||||
{
|
||||
await FillUsers(vm);
|
||||
if (!ModelState.IsValid)
|
||||
@ -163,12 +164,16 @@ namespace BTCPayServer.Controllers
|
||||
ModelState.AddModelError(nameof(vm.Email), "User not found");
|
||||
return View(vm);
|
||||
}
|
||||
if (!StoreRoles.AllRoles.Contains(vm.Role))
|
||||
|
||||
var roles = await _Repo.GetStoreRoles(CurrentStore.Id);
|
||||
if (roles.All(role => role.Id != vm.Role))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Role), "Invalid role");
|
||||
return View(vm);
|
||||
}
|
||||
if (!await _Repo.AddStoreUser(CurrentStore.Id, user.Id, vm.Role))
|
||||
var roleId = await _Repo.ResolveStoreRoleId(storeId, vm.Role);
|
||||
|
||||
if (!await _Repo.AddStoreUser(CurrentStore.Id, user.Id, roleId))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store");
|
||||
return View(vm);
|
||||
@ -611,6 +616,7 @@ namespace BTCPayServer.Controllers
|
||||
Id = store.Id,
|
||||
StoreName = store.StoreName,
|
||||
StoreWebsite = store.StoreWebsite,
|
||||
StoreSupportUrl = storeBlob.StoreSupportUrl,
|
||||
LogoFileId = storeBlob.LogoFileId,
|
||||
CssFileId = storeBlob.CssFileId,
|
||||
BrandColor = storeBlob.BrandColor,
|
||||
@ -646,6 +652,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
var blob = CurrentStore.GetStoreBlob();
|
||||
blob.StoreSupportUrl = model.StoreSupportUrl;
|
||||
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
|
||||
blob.NetworkFeeMode = model.NetworkFeeMode;
|
||||
blob.PaymentTolerance = model.PaymentTolerance;
|
||||
@ -887,8 +894,11 @@ namespace BTCPayServer.Controllers
|
||||
var userId = GetUserId();
|
||||
if (userId == null)
|
||||
return Challenge(AuthenticationSchemes.Cookie);
|
||||
storeId = model.StoreId;
|
||||
var store = CurrentStore ?? await _Repo.FindStore(storeId, userId);
|
||||
var store = model.StoreId switch
|
||||
{
|
||||
null => CurrentStore,
|
||||
string id => await _Repo.FindStore(storeId, userId)
|
||||
};
|
||||
if (store == null)
|
||||
return Challenge(AuthenticationSchemes.Cookie);
|
||||
var tokenRequest = new TokenRequest()
|
||||
@ -906,7 +916,7 @@ namespace BTCPayServer.Controllers
|
||||
Id = tokenRequest.PairingCode,
|
||||
Label = model.Label,
|
||||
});
|
||||
await _TokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, storeId);
|
||||
await _TokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, store.Id);
|
||||
pairingCode = tokenRequest.PairingCode;
|
||||
}
|
||||
else
|
||||
@ -933,8 +943,9 @@ namespace BTCPayServer.Controllers
|
||||
ViewBag.HidePublicKey = true;
|
||||
ViewBag.ShowStores = true;
|
||||
ViewBag.ShowMenu = false;
|
||||
var stores = await _Repo.GetStoresByUserId(userId);
|
||||
model.Stores = new SelectList(stores.Where(s => s.Role == StoreRoles.Owner), nameof(CurrentStore.Id), nameof(CurrentStore.StoreName));
|
||||
var stores = (await _Repo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray();
|
||||
|
||||
model.Stores = new SelectList(stores, nameof(CurrentStore.Id), nameof(CurrentStore.StoreName));
|
||||
if (!model.Stores.Any())
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "You need to be owner of at least one store before pairing";
|
||||
@ -999,14 +1010,14 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
|
||||
}
|
||||
|
||||
var stores = await _Repo.GetStoresByUserId(userId);
|
||||
var stores = (await _Repo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray();
|
||||
return View(new PairingModel
|
||||
{
|
||||
Id = pairing.Id,
|
||||
Label = pairing.Label,
|
||||
SIN = pairing.SIN ?? "Server-Initiated Pairing",
|
||||
StoreId = selectedStore ?? stores.FirstOrDefault()?.Id,
|
||||
Stores = stores.Where(u => u.Role == StoreRoles.Owner).Select(s => new PairingModel.StoreViewModel
|
||||
Stores = stores.Select(s => new PairingModel.StoreViewModel
|
||||
{
|
||||
Id = s.Id,
|
||||
Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName
|
||||
|
@ -39,10 +39,12 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet("create")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettingsUnscoped)]
|
||||
public IActionResult CreateStore()
|
||||
public async Task<IActionResult> CreateStore()
|
||||
{
|
||||
var stores = await _repo.GetStoresByUserId(GetUserId());
|
||||
var vm = new CreateStoreViewModel
|
||||
{
|
||||
IsFirstStore = !stores.Any(),
|
||||
DefaultCurrency = StoreBlob.StandardDefaultCurrency,
|
||||
Exchanges = GetExchangesSelectList(null)
|
||||
};
|
||||
@ -56,6 +58,8 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
var stores = await _repo.GetStoresByUserId(GetUserId());
|
||||
vm.IsFirstStore = !stores.Any();
|
||||
vm.Exchanges = GetExchangesSelectList(vm.PreferredExchange);
|
||||
return View(vm);
|
||||
}
|
||||
|
@ -318,6 +318,8 @@ namespace BTCPayServer.Controllers
|
||||
await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2.0), cloned.ExtractTransaction(), btcPayNetwork);
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(30));
|
||||
var minRelayFee = _dashboard.Get(btcPayNetwork.CryptoCode).Status.BitcoinStatus?.MinRelayTxFee;
|
||||
_payjoinClient.MinimumFeeRate = minRelayFee;
|
||||
return await _payjoinClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, cts.Token);
|
||||
}
|
||||
|
||||
|
@ -189,11 +189,7 @@ namespace BTCPayServer.Controllers
|
||||
ListWalletsViewModel.WalletViewModel walletVm = new ListWalletsViewModel.WalletViewModel();
|
||||
wallets.Wallets.Add(walletVm);
|
||||
walletVm.Balance = await wallet.Balance + " " + wallet.Wallet.Network.CryptoCode;
|
||||
walletVm.IsOwner = wallet.Store.Role == StoreRoles.Owner;
|
||||
if (!walletVm.IsOwner)
|
||||
{
|
||||
walletVm.Balance = "";
|
||||
}
|
||||
|
||||
|
||||
walletVm.CryptoCode = wallet.Network.CryptoCode;
|
||||
walletVm.StoreId = wallet.Store.Id;
|
||||
|
@ -74,6 +74,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
.Include(data => data.PullPaymentData)
|
||||
.ThenInclude(data => data.StoreData)
|
||||
.ThenInclude(data => data.UserStores)
|
||||
.ThenInclude(data => data.StoreRole)
|
||||
.Where(data =>
|
||||
payoutIds.Contains(data.Id) &&
|
||||
data.State == PayoutState.AwaitingPayment &&
|
||||
@ -84,7 +85,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
if (approvedStores.TryGetValue(payout.PullPaymentData.StoreId, out var value))
|
||||
return value;
|
||||
value = payout.PullPaymentData.StoreData.UserStores
|
||||
.Any(store => store.Role == StoreRoles.Owner && store.ApplicationUserId == userId);
|
||||
.Any(store => store.ApplicationUserId == userId && store.StoreRole.Permissions.Contains(Policies.CanModifyStoreSettings));
|
||||
approvedStores.Add(payout.PullPaymentData.StoreId, value);
|
||||
return value;
|
||||
}).ToList();
|
||||
|
@ -65,6 +65,8 @@ namespace BTCPayServer.Data
|
||||
_DefaultCurrency = _DefaultCurrency.Trim().ToUpperInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
public string StoreSupportUrl { get; set; }
|
||||
|
||||
CurrencyPair[] _DefaultCurrencyPairs;
|
||||
[JsonProperty("defaultCurrencyPairs", ItemConverterType = typeof(CurrencyPairJsonConverter))]
|
||||
|
@ -16,32 +16,6 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
public static class StoreDataExtensions
|
||||
{
|
||||
public static PermissionSet GetPermissionSet(this StoreData store)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(store);
|
||||
if (store.Role is null)
|
||||
return new PermissionSet();
|
||||
return new PermissionSet(store.Role == StoreRoles.Owner
|
||||
? new[]
|
||||
{
|
||||
Permission.Create(Policies.CanModifyStoreSettings, store.Id),
|
||||
Permission.Create(Policies.CanTradeCustodianAccount, store.Id),
|
||||
Permission.Create(Policies.CanWithdrawFromCustodianAccounts, store.Id),
|
||||
Permission.Create(Policies.CanDepositToCustodianAccounts, store.Id)
|
||||
}
|
||||
: new[]
|
||||
{
|
||||
Permission.Create(Policies.CanViewStoreSettings, store.Id),
|
||||
Permission.Create(Policies.CanModifyInvoices, store.Id),
|
||||
Permission.Create(Policies.CanViewCustodianAccounts, store.Id),
|
||||
Permission.Create(Policies.CanDepositToCustodianAccounts, store.Id)
|
||||
});
|
||||
}
|
||||
public static bool HasPermission(this StoreData store, string permission)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(store);
|
||||
return store.GetPermissionSet().Contains(permission, store.Id);
|
||||
}
|
||||
|
||||
#pragma warning disable CS0618
|
||||
public static PaymentMethodId? GetDefaultPaymentId(this StoreData storeData)
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
@ -29,12 +30,30 @@ namespace BTCPayServer.Data
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
public WebhookDeliveryStatus Status { get; set; }
|
||||
public int? HttpCode { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string ErrorMessage { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public byte[] Request { get; set; }
|
||||
public void Prune()
|
||||
{
|
||||
var request = JObject.Parse(UTF8Encoding.UTF8.GetString(Request));
|
||||
foreach (var prop in request.Properties().ToList())
|
||||
{
|
||||
if (prop.Name == "type")
|
||||
continue;
|
||||
prop.Remove();
|
||||
}
|
||||
Request = UTF8Encoding.UTF8.GetBytes(request.ToString(Formatting.None));
|
||||
}
|
||||
public T ReadRequestAs<T>()
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(UTF8Encoding.UTF8.GetString(Request), HostedServices.WebhookSender.DefaultSerializerSettings);
|
||||
}
|
||||
|
||||
public bool IsPruned()
|
||||
{
|
||||
return ReadRequestAs<WebhookEvent>().IsPruned();
|
||||
}
|
||||
}
|
||||
public class WebhookBlob
|
||||
{
|
||||
@ -56,11 +75,17 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
public static WebhookDeliveryBlob GetBlob(this WebhookDeliveryData webhook)
|
||||
{
|
||||
return webhook.HasTypedBlob<WebhookDeliveryBlob>().GetBlob(HostedServices.WebhookSender.DefaultSerializerSettings);
|
||||
if (webhook.Blob is null)
|
||||
return null;
|
||||
else
|
||||
return JsonConvert.DeserializeObject<WebhookDeliveryBlob>(webhook.Blob, HostedServices.WebhookSender.DefaultSerializerSettings);
|
||||
}
|
||||
public static void SetBlob(this WebhookDeliveryData webhook, WebhookDeliveryBlob blob)
|
||||
{
|
||||
webhook.HasTypedBlob<WebhookDeliveryBlob>().SetBlob(blob, HostedServices.WebhookSender.DefaultSerializerSettings);
|
||||
if (blob is null)
|
||||
webhook.Blob = null;
|
||||
else
|
||||
webhook.Blob = JsonConvert.SerializeObject(blob, HostedServices.WebhookSender.DefaultSerializerSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@ -86,9 +87,6 @@ namespace BTCPayServer
|
||||
}
|
||||
}
|
||||
|
||||
var log = evt.ToString();
|
||||
if (!String.IsNullOrEmpty(log))
|
||||
Logs.Events.LogInformation(log);
|
||||
foreach (var sub in actionList)
|
||||
{
|
||||
try
|
||||
|
@ -13,6 +13,7 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models;
|
||||
@ -25,6 +26,7 @@ using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Payment;
|
||||
@ -115,6 +117,14 @@ namespace BTCPayServer
|
||||
return value;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddScheduledTask<T>(this IServiceCollection services, TimeSpan every)
|
||||
where T : class, IPeriodicTask
|
||||
{
|
||||
services.AddSingleton<T>();
|
||||
services.AddTransient<ScheduledTask>(o => new ScheduledTask(typeof(T), every));
|
||||
return services;
|
||||
}
|
||||
|
||||
public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info)
|
||||
{
|
||||
return new PaymentMethodId(info.CryptoCode, PaymentTypes.Parse(info.PaymentType));
|
||||
|
@ -1,11 +1,40 @@
|
||||
#nullable enable
|
||||
using System.Linq;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public static class StoreExtensions
|
||||
{
|
||||
public static StoreRole? GetStoreRoleOfUser(this StoreData store, string userId)
|
||||
{
|
||||
return store.UserStores.FirstOrDefault(r => r.ApplicationUserId == userId)?.StoreRole;
|
||||
}
|
||||
|
||||
public static PermissionSet GetPermissionSet(this StoreRole storeRole, string storeId)
|
||||
{
|
||||
return new PermissionSet(storeRole.Permissions
|
||||
.Select(s => Permission.TryCreatePermission(s, storeId, out var permission) ? permission : null)
|
||||
.Where(s => s != null).ToArray());
|
||||
}
|
||||
|
||||
|
||||
public static PermissionSet GetPermissionSet(this StoreData store, string userId)
|
||||
{
|
||||
return store.GetStoreRoleOfUser(userId)?.GetPermissionSet(store.Id)?? new PermissionSet();
|
||||
}
|
||||
|
||||
public static bool HasPermission(this StoreData store, string userId, string permission)
|
||||
{
|
||||
return GetPermissionSet(store, userId).HasPermission(permission, store.Id);
|
||||
}
|
||||
|
||||
public static bool HasPermission(this PermissionSet permissionSet, string permission, string storeId)
|
||||
{
|
||||
return permissionSet.Contains(permission, storeId);
|
||||
}
|
||||
|
||||
public static DerivationSchemeSettings? GetDerivationSchemeSettings(this StoreData store, BTCPayNetworkProvider networkProvider, string cryptoCode)
|
||||
{
|
||||
var paymentMethod = store
|
||||
|
26
BTCPayServer/Forms/FieldValueMirror.cs
Normal file
26
BTCPayServer/Forms/FieldValueMirror.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
public class FieldValueMirror : IFormComponentProvider
|
||||
{
|
||||
public string View { get; } = null;
|
||||
public void Validate(Form form, Field field)
|
||||
{
|
||||
if (form.GetFieldByFullName(field.Value) is null)
|
||||
{
|
||||
field.ValidationErrors = new List<string> { $"{field.Name} requires {field.Value} to be present" };
|
||||
}
|
||||
}
|
||||
|
||||
public void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)
|
||||
{
|
||||
typeToComponentProvider.Add("mirror", this);
|
||||
}
|
||||
|
||||
public string GetValue(Form form, Field field)
|
||||
{
|
||||
return form.GetFieldByFullName(field.Value)?.Value;
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ public static class FormDataExtensions
|
||||
serviceCollection.AddSingleton<FormDataService>();
|
||||
serviceCollection.AddSingleton<FormComponentProviders>();
|
||||
serviceCollection.AddSingleton<IFormComponentProvider, HtmlInputFormProvider>();
|
||||
serviceCollection.AddSingleton<IFormComponentProvider, HtmlTextareaFormProvider>();
|
||||
serviceCollection.AddSingleton<IFormComponentProvider, HtmlFieldsetFormProvider>();
|
||||
serviceCollection.AddSingleton<IFormComponentProvider, HtmlSelectFormProvider>();
|
||||
serviceCollection.AddSingleton<IFormComponentProvider, FieldValueMirror>();
|
||||
|
@ -5,27 +5,6 @@ using BTCPayServer.Validation;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
public class FieldValueMirror : IFormComponentProvider
|
||||
{
|
||||
public string View { get; } = null;
|
||||
public void Validate(Form form, Field field)
|
||||
{
|
||||
if (form.GetFieldByFullName(field.Value) is null)
|
||||
{
|
||||
field.ValidationErrors = new List<string>() { $"{field.Name} requires {field.Value} to be present" };
|
||||
}
|
||||
}
|
||||
|
||||
public void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)
|
||||
{
|
||||
typeToComponentProvider.Add("mirror", this);
|
||||
}
|
||||
|
||||
public string GetValue(Form form, Field field)
|
||||
{
|
||||
return form.GetFieldByFullName(field.Value)?.Value;
|
||||
}
|
||||
}
|
||||
public class HtmlInputFormProvider : FormComponentProviderBase
|
||||
{
|
||||
public override void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)
|
||||
|
@ -9,10 +9,9 @@ public class HtmlSelectFormProvider : FormComponentProviderBase
|
||||
{
|
||||
public override void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)
|
||||
{
|
||||
foreach (var t in new[] {
|
||||
"select"})
|
||||
typeToComponentProvider.Add(t, this);
|
||||
typeToComponentProvider.Add("select", this);
|
||||
}
|
||||
|
||||
public override string View => "Forms/SelectElement";
|
||||
|
||||
public override void Validate(Form form, Field field)
|
||||
|
23
BTCPayServer/Forms/HtmlTextareaFormProvider.cs
Normal file
23
BTCPayServer/Forms/HtmlTextareaFormProvider.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
public class HtmlTextareaFormProvider : FormComponentProviderBase
|
||||
{
|
||||
public override void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)
|
||||
{
|
||||
typeToComponentProvider.Add("textarea", this);
|
||||
}
|
||||
|
||||
public override string View => "Forms/TextareaElement";
|
||||
|
||||
public override void Validate(Form form, Field field)
|
||||
{
|
||||
if (field.Required)
|
||||
{
|
||||
ValidateField<RequiredAttribute>(field);
|
||||
}
|
||||
}
|
||||
}
|
@ -77,7 +77,6 @@ public class UIFormsController : Controller
|
||||
|
||||
if (!_formDataService.IsFormSchemaValid(modifyForm.FormConfig, out var form, out var error))
|
||||
{
|
||||
|
||||
ModelState.AddModelError(nameof(modifyForm.FormConfig),
|
||||
$"Form config was invalid: {error})");
|
||||
}
|
||||
@ -86,7 +85,6 @@ public class UIFormsController : Controller
|
||||
modifyForm.FormConfig = form.ToString();
|
||||
}
|
||||
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(modifyForm);
|
||||
|
@ -40,11 +40,11 @@ namespace BTCPayServer.HostedServices
|
||||
case PointOfSaleAppType.AppType:
|
||||
var possettings = data.GetSettings<PointOfSaleSettings>();
|
||||
return (Data: data, Settings: (object)possettings,
|
||||
Items: _appService.Parse(possettings.Template, possettings.Currency));
|
||||
Items: AppService.Parse(possettings.Template));
|
||||
case CrowdfundAppType.AppType:
|
||||
var cfsettings = data.GetSettings<CrowdfundSettings>();
|
||||
return (Data: data, Settings: (object)cfsettings,
|
||||
Items: _appService.Parse(cfsettings.PerksTemplate, cfsettings.TargetCurrency));
|
||||
Items: AppService.Parse(cfsettings.PerksTemplate));
|
||||
default:
|
||||
return (null, null, null);
|
||||
}
|
||||
@ -70,11 +70,11 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
case PointOfSaleAppType.AppType:
|
||||
((PointOfSaleSettings)valueTuple.Settings).Template =
|
||||
_appService.SerializeTemplate(valueTuple.Items);
|
||||
AppService.SerializeTemplate(valueTuple.Items);
|
||||
break;
|
||||
case CrowdfundAppType.AppType:
|
||||
((CrowdfundSettings)valueTuple.Settings).PerksTemplate =
|
||||
_appService.SerializeTemplate(valueTuple.Items);
|
||||
AppService.SerializeTemplate(valueTuple.Items);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
|
@ -261,7 +261,6 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
if (e.Name == InvoiceEvent.Expired ||
|
||||
e.Name == InvoiceEvent.PaidInFull ||
|
||||
e.Name == InvoiceEvent.FailedToConfirm ||
|
||||
e.Name == InvoiceEvent.MarkedInvalid ||
|
||||
e.Name == InvoiceEvent.MarkedCompleted ||
|
||||
e.Name == InvoiceEvent.FailedToConfirm ||
|
||||
|
61
BTCPayServer/HostedServices/CleanupWebhookDeliveriesTask.cs
Normal file
61
BTCPayServer/HostedServices/CleanupWebhookDeliveriesTask.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using Dapper;
|
||||
using System.Data.Common;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class CleanupWebhookDeliveriesTask : IPeriodicTask
|
||||
{
|
||||
public CleanupWebhookDeliveriesTask(ApplicationDbContextFactory dbContextFactory)
|
||||
{
|
||||
DbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public ApplicationDbContextFactory DbContextFactory { get; }
|
||||
public int BatchSize { get; set; } = 500;
|
||||
public TimeSpan PruneAfter { get; set; } = TimeSpan.FromDays(60);
|
||||
|
||||
public async Task Do(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var ctx = DbContextFactory.CreateContext();
|
||||
if (!ctx.Database.IsNpgsql())
|
||||
return;
|
||||
var conn = ctx.Database.GetDbConnection();
|
||||
bool pruned = false;
|
||||
int offset = 0;
|
||||
retry:
|
||||
var rows = await conn.QueryAsync<WebhookDeliveryData>(@"
|
||||
SELECT ""Id"", ""Blob""
|
||||
FROM ""WebhookDeliveries""
|
||||
WHERE ((now() - ""Timestamp"") > @PruneAfter) AND ""Pruned"" IS FALSE
|
||||
ORDER BY ""Timestamp""
|
||||
LIMIT @BatchSize OFFSET @offset
|
||||
", new { PruneAfter, BatchSize, offset });
|
||||
|
||||
foreach (var d in rows)
|
||||
{
|
||||
var blob = d.GetBlob();
|
||||
blob.Prune();
|
||||
d.SetBlob(blob);
|
||||
d.Pruned = true;
|
||||
pruned = true;
|
||||
}
|
||||
if (pruned)
|
||||
{
|
||||
pruned = false;
|
||||
await conn.ExecuteAsync("UPDATE \"WebhookDeliveries\" SET \"Blob\"=@Blob::JSONB, \"Pruned\"=@Pruned WHERE \"Id\"=@Id", rows);
|
||||
if (rows.Count() == BatchSize)
|
||||
{
|
||||
offset += BatchSize;
|
||||
goto retry;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
BTCPayServer/HostedServices/IPeriodicTask.cs
Normal file
10
BTCPayServer/HostedServices/IPeriodicTask.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public interface IPeriodicTask
|
||||
{
|
||||
Task Do(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
@ -380,7 +380,8 @@ namespace BTCPayServer.HostedServices
|
||||
if ((onChainPaymentData.ConfirmationCount < network.MaxTrackedConfirmation && payment.Accounted)
|
||||
&& (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
||||
{
|
||||
var transactionResult = await _explorerClientProvider.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash);
|
||||
var client = _explorerClientProvider.GetExplorerClient(payment.GetCryptoCode());
|
||||
var transactionResult = client is null ? null : await client.GetTransactionAsync(onChainPaymentData.Outpoint.Hash);
|
||||
var confirmationCount = transactionResult?.Confirmations ?? 0;
|
||||
onChainPaymentData.ConfirmationCount = confirmationCount;
|
||||
payment.SetCryptoPaymentData(onChainPaymentData);
|
||||
|
@ -0,0 +1,92 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class PeriodicTaskLauncherHostedService : IHostedService
|
||||
{
|
||||
public PeriodicTaskLauncherHostedService(IServiceProvider serviceProvider, ILoggerFactory loggerFactory)
|
||||
{
|
||||
ServiceProvider = serviceProvider;
|
||||
Logger = loggerFactory.CreateLogger("BTCPayServer.PeriodicTasks");
|
||||
}
|
||||
|
||||
public IServiceProvider ServiceProvider { get; }
|
||||
public ILogger Logger { get; }
|
||||
|
||||
Channel<ScheduledTask> jobs = Channel.CreateBounded<ScheduledTask>(100);
|
||||
CancellationTokenSource cts;
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cts = new CancellationTokenSource();
|
||||
foreach (var task in ServiceProvider.GetServices<ScheduledTask>())
|
||||
jobs.Writer.TryWrite(task);
|
||||
|
||||
loop = Task.WhenAll(Enumerable.Range(0, 3).Select(_ => Loop(cts.Token)).ToArray());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
Task loop;
|
||||
private async Task Loop(CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
await foreach (var job in jobs.Reader.ReadAllAsync(token))
|
||||
{
|
||||
if (job.NextScheduled <= DateTimeOffset.UtcNow)
|
||||
{
|
||||
var t = (IPeriodicTask)ServiceProvider.GetService(job.PeriodicTaskType);
|
||||
try
|
||||
{
|
||||
await t.Do(token);
|
||||
}
|
||||
catch when (token.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, $"Unhandled error in periodic task {job.PeriodicTaskType.Name}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
job.NextScheduled = DateTimeOffset.UtcNow + job.Every;
|
||||
}
|
||||
}
|
||||
_ = Wait(job, token);
|
||||
}
|
||||
}
|
||||
catch when (token.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Wait(ScheduledTask job, CancellationToken token)
|
||||
{
|
||||
var timeToWait = job.NextScheduled - DateTimeOffset.UtcNow;
|
||||
try
|
||||
{
|
||||
await Task.Delay(timeToWait, token);
|
||||
}
|
||||
catch { }
|
||||
while (await jobs.Writer.WaitToWriteAsync())
|
||||
{
|
||||
if (jobs.Writer.TryWrite(job))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cts?.Cancel();
|
||||
jobs.Writer.TryComplete();
|
||||
if (loop is not null)
|
||||
await loop;
|
||||
}
|
||||
}
|
||||
}
|
16
BTCPayServer/HostedServices/ScheduledTask.cs
Normal file
16
BTCPayServer/HostedServices/ScheduledTask.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class ScheduledTask
|
||||
{
|
||||
public ScheduledTask(Type periodicTypeTask, TimeSpan every)
|
||||
{
|
||||
PeriodicTaskType = periodicTypeTask;
|
||||
Every = every;
|
||||
}
|
||||
public Type PeriodicTaskType { get; set; }
|
||||
public TimeSpan Every { get; set; } = TimeSpan.FromMinutes(5.0);
|
||||
public DateTimeOffset NextScheduled { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user