Compare commits
103 Commits
bumpbtc
...
lnurlfixes
Author | SHA1 | Date | |
---|---|---|---|
96c2fd7244 | |||
73fbfbd7cb | |||
acae3b8753 | |||
a618f901fc | |||
6d4918f0ab | |||
7f2c4d2e7a | |||
fd6d361e1a | |||
b5f0924651 | |||
1600dd4759 | |||
c777746b69 | |||
9f5466a41f | |||
4d1e4801bf | |||
5e469ff9c0 | |||
2f3eedea5b | |||
5c5d6dc1e2 | |||
fbe31ce64f | |||
0b082138c8 | |||
966e598f10 | |||
e998340387 | |||
f6b27cc5f9 | |||
f3dbf1e139 | |||
627d84fc91 | |||
8cde8c01df | |||
983b8c1f54 | |||
d666d8ea1a | |||
3ed81c3a78 | |||
4afec2e2b6 | |||
db83d238d5 | |||
fdcf7b3b7a | |||
53aafcf86b | |||
aec84f6d67 | |||
01e9f82d24 | |||
2eff45e65c | |||
13203c3e2b | |||
82c5e0e43d | |||
a1575f404b | |||
e1509506dc | |||
0c1d0d7b05 | |||
ad70856af0 | |||
8615f120ce | |||
0d0477d661 | |||
b31dc30878 | |||
6e392f4cfb | |||
cc3bdc331e | |||
76faf77a1c | |||
d8c0e5bf3a | |||
28c4c320cc | |||
e81403ec3f | |||
f11424f73a | |||
fa8b977016 | |||
d181846339 | |||
1956919886 | |||
0f66498965 | |||
918cd152b1 | |||
d3222df396 | |||
a84ffd8c7e | |||
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 |
.github
BTCPayServer.Abstractions
BTCPayServer.Client
BTCPayServer.Client.csprojBTCPayServerClient.ServerInfo.csBTCPayServerClient.StoreUsers.csBTCPayServerClient.cs
Models
Permissions.csBTCPayServer.Common
Altcoins
BTCPayNetworkProvider.BGold.csBTCPayNetworkProvider.Bitcore.csBTCPayNetworkProvider.Chaincoin.cs
BTCPayNetworkProvider.csBTCPayServer.Common.csprojLiquid
Monero/RPC
BTCPayServer.Data
BTCPayServer.Rating
BTCPayServer.Tests
AltcoinTests
CrowdfundTests.csExtensions.csFastTests.csGreenfieldAPITests.csPOSTests.csSeleniumTests.csTestAccount.csThirdPartyTests.csUnitTest1.csdocker-compose.altcoins.ymldocker-compose.ymlBTCPayServer
BTCPayServer.csprojStorePolicies.cs
Components
AppTopItems
MainNav
StoreLightningBalance
StoreRecentInvoices
StoreRecentTransactions
StoreSelector
StoreWalletBalance
Controllers
GreenField
GreenfieldAppsController.csGreenfieldInvoiceController.csGreenfieldPaymentRequestsController.csGreenfieldPullPaymentController.csGreenfieldServerRolesController.csGreenfieldStoreOnChainWalletsController.csGreenfieldStoreRolesController.csGreenfieldStoreUsersController.csGreenfieldStoreWebhooksController.csGreenfieldStoresController.csLocalBTCPayServerClient.cs
UIAppsController.csUIHomeController.csUIInvoiceController.UI.csUIInvoiceController.csUILNURLController.csUIManageController.APIKeys.csUIPaymentRequestController.csUIPullPaymentController.csUIServerController.Roles.csUIServerController.csUIStoresController.Onchain.csUIStoresController.Roles.csUIStoresController.csUIWalletsController.PSBT.csUIWalletsController.csData
Extensions.csExtensions
Forms
HostedServices
AppInventoryUpdaterHostedService.csCleanupWebhookDeliveriesTask.csIPeriodicTask.csPeriodicTaskLauncherHostedService.csPullPaymentHostedService.csScheduledTask.csWebhookSender.cs
Hosting
Models
AppViewModels
PaymentRequestViewModels
ServerViewModels
StoreViewModels
ViewPullPaymentModel.csWalletViewModels
Payments/Lightning
PayoutProcessors
Plugins
Crowdfund
PayButton/Controllers
PointOfSale
Security
Services
Altcoins/Monero
Apps
Invoices
MigrationSettings.csPoliciesSettings.csStores
UserService.csWalletRepository.csWallets
Views
Shared
CreateOrEditRole.cshtml
Crowdfund
Forms
LayoutHeadTheme.cshtmlListRoles.cshtmlNFC
PayButton
PointOfSale
PosData.cshtmlShowQR.cshtmlTemplateEditor.cshtml_Layout.cshtmlUIApps
UIForms
UIHome
UIInvoice
CheckoutV2.cshtmlInvoice.cshtmlInvoiceReceipt.cshtmlInvoiceReceiptPrint.cshtmlListInvoices.cshtml_RefundModal.cshtml
UIManage
UIPaymentRequest
UIPullPayment
UIServer
UIStores
Dashboard.cshtmlModifyWebhook.cshtmlStoreNavPages.csStoreUsers.cshtml_GenerateWalletForm.cshtml_LayoutWalletSetup.cshtml_Nav.cshtml
UIWallets
wwwroot
cart
checkout-v2
crowdfund
img
js
light-pos
locales
main
modal
paybutton
swagger/v1
vendor
bootstrap-vue
bootstrap-vue.cssbootstrap-vue.jsbootstrap-vue.min.cssbootstrap-vue.min.jsbootstrap-vue.min.min.css.mapbootstrap-vue.min.min.js.map
bootstrap
vue-sanitize-directive
vue-sortable
Build
Changelog.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)
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ namespace BTCPayServer
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"BTG_X = BTG_BTC * BTC_X",
|
||||
"BTG_BTC = bitfinex(BTG_BTC)",
|
||||
"BTG_BTC = exmo(BTG_BTC)",
|
||||
},
|
||||
CryptoImagePath = "imlegacy/btg.svg",
|
||||
LightningImagePath = "imlegacy/btg-lightning.svg",
|
||||
|
@ -17,7 +17,7 @@ namespace BTCPayServer
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"BTX_X = BTX_BTC * BTC_X",
|
||||
"BTX_BTC = hitbtc(BTX_BTC)"
|
||||
"BTX_BTC = graviex(BTX_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/bitcore.svg",
|
||||
LightningImagePath = "imlegacy/bitcore-lightning.svg",
|
||||
|
@ -1,32 +0,0 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitChaincoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("CHC");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Chaincoin",
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet
|
||||
? "https://explorer.chaincoin.org/Explorer/Transaction/{0}"
|
||||
: "https://test.explorer.chaincoin.org/Explorer/Transaction/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"CHC_X = CHC_BTC * BTC_X",
|
||||
"CHC_BTC = txbit(CHC_X)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/chaincoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
//https://github.com/satoshilabs/slips/blob/master/slip-0044.md
|
||||
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("711'")
|
||||
: new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -63,6 +63,7 @@ namespace BTCPayServer
|
||||
"LCAD_CAD = 1",
|
||||
"LCAD_X = CAD_BTC * BTC_X",
|
||||
"LCAD_BTC = bylls(CAD_BTC)",
|
||||
"CAD_BTC = LCAD_BTC"
|
||||
},
|
||||
AssetId = new uint256("0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a"),
|
||||
DisplayName = "Liquid CAD",
|
||||
|
@ -45,10 +45,10 @@ namespace BTCPayServer.Services.Altcoins.Monero.RPC
|
||||
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic",
|
||||
Convert.ToBase64String(Encoding.Default.GetBytes($"{_username}:{_password}")));
|
||||
|
||||
var rawResult = await _httpClient.SendAsync(httpRequest, cts);
|
||||
|
||||
var rawJson = await rawResult.Content.ReadAsStringAsync();
|
||||
HttpResponseMessage rawResult = await _httpClient.SendAsync(httpRequest, cts);
|
||||
rawResult.EnsureSuccessStatusCode();
|
||||
var rawJson = await rawResult.Content.ReadAsStringAsync();
|
||||
|
||||
JsonRpcResult<TResponse> response;
|
||||
try
|
||||
{
|
||||
|
@ -56,7 +56,6 @@ namespace BTCPayServer
|
||||
InitViacoin();
|
||||
InitMonero();
|
||||
InitZcash();
|
||||
InitChaincoin();
|
||||
// InitArgoneum();//their rate source is down 9/15/20.
|
||||
// InitMonetaryUnit(); Not supported from Bittrex from 11/23/2022, dead shitcoin
|
||||
|
||||
|
@ -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");
|
||||
|
@ -15,7 +15,7 @@ namespace BTCPayServer.Rating
|
||||
while (true)
|
||||
{
|
||||
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
|
||||
if ((Math.Abs(rounded - value) / value) < 0.001m)
|
||||
if ((Math.Abs(rounded - value) / value) < 0.01m)
|
||||
{
|
||||
value = rounded;
|
||||
break;
|
||||
|
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Rates;
|
||||
|
||||
|
||||
public class ExchangeRateHostRateProvider : IRateProvider
|
||||
{
|
||||
public RateSourceInfo RateSourceInfo => new("exchangeratehost", "Yadio", "https://api.exchangerate.host/latest?base=BTC");
|
||||
private readonly HttpClient _httpClient;
|
||||
public ExchangeRateHostRateProvider(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _httpClient.GetAsync(RateSourceInfo.Url, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
||||
if(jobj["success"].Value<bool>() is not true || !jobj["base"].Value<string>().Equals("BTC", StringComparison.InvariantCulture))
|
||||
throw new Exception("exchangerate.host returned a non success response or the base currency was not the requested one (BTC)");
|
||||
var results = (JObject) jobj["rates"] ;
|
||||
//key value is currency code to rate value
|
||||
var list = new List<PairRate>();
|
||||
foreach (var item in results)
|
||||
{
|
||||
string name = item.Key;
|
||||
var value = item.Value.Value<decimal>();
|
||||
list.Add(new PairRate(new CurrencyPair("BTC", name), new BidAsk(value)));
|
||||
}
|
||||
|
||||
return list.ToArray();
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Rates;
|
||||
|
||||
public class FreeCurrencyRatesRateProvider : IRateProvider
|
||||
{
|
||||
public RateSourceInfo RateSourceInfo => new("free-currency-rates", "Free Currency Rates", "https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies/btc.min.json");
|
||||
private readonly HttpClient _httpClient;
|
||||
public FreeCurrencyRatesRateProvider(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _httpClient.GetAsync(RateSourceInfo.Url, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
||||
var results = (JObject) jobj["btc"] ;
|
||||
//key value is currency code to rate value
|
||||
var list = new List<PairRate>();
|
||||
foreach (var item in results)
|
||||
{
|
||||
string name = item.Key;
|
||||
var value = item.Value.Value<decimal>();
|
||||
list.Add(new PairRate(new CurrencyPair("BTC", name), new BidAsk(value)));
|
||||
}
|
||||
|
||||
return list.ToArray();
|
||||
}
|
||||
}
|
@ -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;
|
||||
@ -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,13 +664,12 @@ 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);
|
||||
Assert.Equal("15,18,20", string.Join(',', vmview.CustomTipPercentages));
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "orange").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "orange").Result);
|
||||
|
||||
//
|
||||
var invoices = await user.BitPay.GetInvoicesAsync();
|
||||
@ -677,7 +678,7 @@ donation:
|
||||
Assert.Equal("CAD", orangeInvoice.Currency);
|
||||
Assert.Equal("orange", orangeInvoice.ItemDesc);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "apple").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "apple").Result);
|
||||
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple"));
|
||||
@ -686,7 +687,7 @@ donation:
|
||||
|
||||
// testing custom amount
|
||||
var action = Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 6.6m, null, null, null, null, "donation").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 6.6m, choiceKey: "donation").Result);
|
||||
Assert.Equal(nameof(UIInvoiceController.Checkout), action.ActionName);
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
var donationInvoice = invoices.Single(i => i.Price == 6.6m);
|
||||
@ -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,26 +752,28 @@ 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
|
||||
await tester.WaitForEvent<AppInventoryUpdaterHostedService.UpdateAppInventory>(() =>
|
||||
{
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "inventoryitem").Result);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
//we already bought all available stock so this should fail
|
||||
await Task.Delay(100);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "inventoryitem").Result);
|
||||
|
||||
//inventoryitem has unlimited items available
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "noninventoryitem").Result);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "noninventoryitem").Result);
|
||||
|
||||
//verify invoices where created
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
@ -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,11 +805,13 @@ 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);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "btconly").Result);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "normal").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "normal").Result);
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
var normalInvoice = invoices.Single(invoice => invoice.ItemCode == "normal");
|
||||
var btcOnlyInvoice = invoices.Single(invoice => invoice.ItemCode == "btconly");
|
||||
@ -847,20 +851,21 @@ 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);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Static, choiceKey: "g").Result);
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
var topupInvoice = invoices.Single(invoice => invoice.ItemCode == "g");
|
||||
Assert.Equal(0, topupInvoice.Price);
|
||||
|
@ -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));
|
||||
|
@ -122,6 +122,13 @@ retry:
|
||||
driver.ExecuteJavaScript($"document.getElementById('{element}').{funcName}()");
|
||||
}
|
||||
|
||||
public static void WaitWalletTransactionsLoaded(this IWebDriver driver)
|
||||
{
|
||||
var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait);
|
||||
wait.UntilJsIsReady();
|
||||
wait.Until(d => d.WaitForElement(By.CssSelector("#WalletTransactions[data-loaded='true']")));
|
||||
}
|
||||
|
||||
public static IWebElement WaitForElement(this IWebDriver driver, By selector)
|
||||
{
|
||||
var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait);
|
||||
|
@ -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;
|
||||
@ -1144,6 +1145,45 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("000000161", m.OrderId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseOldPosAppData()
|
||||
{
|
||||
var data = new JObject()
|
||||
{
|
||||
["price"] = 1.64m
|
||||
}.ToString();
|
||||
Assert.Equal(1.64m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
|
||||
|
||||
data = new JObject()
|
||||
{
|
||||
["price"] = new JObject()
|
||||
{
|
||||
["value"] = 1.65m
|
||||
}
|
||||
}.ToString();
|
||||
Assert.Equal(1.65m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
|
||||
data = new JObject()
|
||||
{
|
||||
["price"] = new JObject()
|
||||
{
|
||||
["value"] = "1.6305"
|
||||
}
|
||||
}.ToString();
|
||||
Assert.Equal(1.6305m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
|
||||
|
||||
data = new JObject()
|
||||
{
|
||||
["price"] = new JObject()
|
||||
{
|
||||
["value"] = null
|
||||
}
|
||||
}.ToString();
|
||||
Assert.Equal(0.0m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
|
||||
|
||||
var o = JObject.Parse(JsonConvert.SerializeObject(new PosAppCartItem() { Price = 1.356m }));
|
||||
Assert.Equal(1.356m, o["price"].Value<decimal>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseCurrencyValue()
|
||||
{
|
||||
@ -1382,6 +1422,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()
|
||||
{
|
||||
|
@ -21,6 +21,7 @@ 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;
|
||||
@ -228,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);
|
||||
}
|
||||
|
||||
@ -1073,6 +1074,22 @@ namespace BTCPayServer.Tests
|
||||
var lnrURLs = await unauthenticated.GetPullPaymentLNURL(test4.Id);
|
||||
Assert.IsType<string>(lnrURLs.LNURLBech32);
|
||||
Assert.IsType<string>(lnrURLs.LNURLUri);
|
||||
Assert.Equal(12.303228134m, test4.Amount);
|
||||
Assert.Equal("BTC", test4.Currency);
|
||||
|
||||
// Test with SATS denomination values
|
||||
var testSats = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
|
||||
{
|
||||
Name = "Test SATS",
|
||||
Amount = 21000,
|
||||
Currency = "SATS",
|
||||
PaymentMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
|
||||
});
|
||||
lnrURLs = await unauthenticated.GetPullPaymentLNURL(testSats.Id);
|
||||
Assert.IsType<string>(lnrURLs.LNURLBech32);
|
||||
Assert.IsType<string>(lnrURLs.LNURLUri);
|
||||
Assert.Equal(21000, testSats.Amount);
|
||||
Assert.Equal("SATS", testSats.Currency);
|
||||
|
||||
//permission test around auto approved pps and payouts
|
||||
var nonApproved = await acc.CreateClient(Policies.CanCreateNonApprovedPullPayments);
|
||||
@ -1319,7 +1336,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" }));
|
||||
|
||||
@ -1430,7 +1448,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();
|
||||
@ -1507,6 +1525,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"));
|
||||
@ -2583,7 +2609,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",
|
||||
@ -2594,18 +2625,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.True(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)]
|
||||
@ -3343,11 +3391,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);
|
||||
|
||||
@ -3358,7 +3411,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));
|
||||
@ -3372,10 +3425,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);
|
||||
|
||||
|
||||
|
@ -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>();
|
||||
@ -64,10 +135,10 @@ donation:
|
||||
Assert.Equal("donation", vmview.Items[1].Title);
|
||||
// orange is available
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "orange").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "orange").Result);
|
||||
// apple is not found
|
||||
Assert.IsType<NotFoundResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "apple").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "apple").Result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
@ -129,16 +131,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 +163,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;
|
||||
@ -952,11 +962,13 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Cart']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".template-item:nth-of-type(1) .btn-primary")).Click();
|
||||
s.Driver.FindElement(By.Id("BuyButtonText")).SendKeys("Take my money");
|
||||
s.Driver.FindElement(By.Id("EditorCategories-ts-control")).SendKeys("Drinks");
|
||||
s.Driver.FindElement(By.Id("SaveItemChanges")).Click();
|
||||
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);
|
||||
Assert.Matches("\"categories\": \\[\n\\s+\"Drinks\"\n\\s+\\]", template);
|
||||
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
@ -970,6 +982,14 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(s.Driver.PageSource.Contains("Tea shop"), "Unable to create PoS");
|
||||
Assert.True(s.Driver.PageSource.Contains("Cart"), "PoS not showing correct default view");
|
||||
Assert.True(s.Driver.PageSource.Contains("Take my money"), "PoS not showing correct default view");
|
||||
Assert.Equal(5, s.Driver.FindElements(By.CssSelector(".card-deck .card:not(.d-none)")).Count);
|
||||
|
||||
var drinks = s.Driver.FindElement(By.CssSelector("label[for='Category-Drinks']"));
|
||||
Assert.Equal("Drinks", drinks.Text);
|
||||
drinks.Click();
|
||||
Assert.Single(s.Driver.FindElements(By.CssSelector(".card-deck .card:not(.d-none)")));
|
||||
s.Driver.FindElement(By.CssSelector("label[for='Category-*']")).Click();
|
||||
Assert.Equal(5, s.Driver.FindElements(By.CssSelector(".card-deck .card:not(.d-none)")).Count);
|
||||
|
||||
s.Driver.Url = posBaseUrl + "/static";
|
||||
Assert.False(s.Driver.PageSource.Contains("Cart"), "Static PoS not showing correct view");
|
||||
@ -1136,12 +1156,13 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("ArchivePaymentRequest")).Click();
|
||||
Assert.Contains("The payment request has been archived", s.FindAlertMessage().Text);
|
||||
Assert.DoesNotContain("Pay123", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Id("SearchDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("SearchIncludeArchived")).Click();
|
||||
s.Driver.FindElement(By.Id("StatusOptionsToggle")).Click();
|
||||
s.Driver.WaitForElement(By.Id("StatusOptionsIncludeArchived")).Click();
|
||||
Assert.Contains("Pay123", s.Driver.PageSource);
|
||||
|
||||
// unarchive (from list)
|
||||
s.Driver.FindElement(By.Id($"ToggleArchival-{payReqId}")).Click();
|
||||
s.Driver.FindElement(By.Id($"ToggleActions-{payReqId}")).Click();
|
||||
s.Driver.WaitForElement(By.Id($"ToggleArchival-{payReqId}")).Click();
|
||||
Assert.Contains("The payment request has been unarchived", s.FindAlertMessage().Text);
|
||||
Assert.Contains("Pay123", s.Driver.PageSource);
|
||||
}
|
||||
@ -1432,7 +1453,7 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("CancelWizard")).Click();
|
||||
|
||||
// Check the label is applied to the tx
|
||||
|
||||
s.Driver.WaitWalletTransactionsLoaded();
|
||||
Assert.Equal("label2", s.Driver.FindElement(By.XPath("//*[@id=\"WalletTransactionsList\"]//*[contains(@class, 'transaction-label')]")).Text);
|
||||
|
||||
//change the wallet and ensure old address is not there and generating a new one does not result in the prev one
|
||||
@ -1483,7 +1504,9 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Check the tx sent earlier arrived
|
||||
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
|
||||
Assert.Contains(tx.ToString(), s.Driver.PageSource);
|
||||
s.Driver.WaitWalletTransactionsLoaded();
|
||||
s.Driver.FindElement(By.PartialLinkText(tx.ToString()));
|
||||
|
||||
var walletTransactionUri = new Uri(s.Driver.Url);
|
||||
|
||||
// Send to bob
|
||||
@ -1609,9 +1632,8 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Transactions list is empty
|
||||
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
|
||||
Assert.Contains("There are no transactions yet.", s.Driver.PageSource);
|
||||
s.Driver.AssertElementNotFound(By.Id("ExportDropdownToggle"));
|
||||
s.Driver.AssertElementNotFound(By.Id("ActionsDropdownToggle"));
|
||||
s.Driver.WaitWalletTransactionsLoaded();
|
||||
Assert.Contains("There are no transactions yet", s.Driver.FindElement(By.Id("WalletTransactions")).Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -1733,12 +1755,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();
|
||||
ReadOnlyCollection<IWebElement> txs;
|
||||
@ -1922,8 +1944,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Assert.Contains(PayoutState.AwaitingPayment.GetStateString(), s.Driver.PageSource);
|
||||
|
||||
//lnurl-w support check
|
||||
|
||||
// LNURL Withdraw support check with BTC denomination
|
||||
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
||||
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
|
||||
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
|
||||
@ -1991,6 +2012,42 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Assert.Contains(PayoutState.AwaitingApproval.GetStateString(), s.Driver.PageSource);
|
||||
});
|
||||
|
||||
// LNURL Withdraw support check with SATS denomination
|
||||
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
||||
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
|
||||
s.Driver.FindElement(By.Id("Name")).SendKeys("PP SATS");
|
||||
s.Driver.SetCheckbox(By.Id("AutoApproveClaims"), true);
|
||||
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("21021");
|
||||
s.Driver.FindElement(By.Id("Currency")).Clear();
|
||||
s.Driver.FindElement(By.Id("Currency")).SendKeys("SATS" + Keys.Enter);
|
||||
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("#lnurlwithdraw-button")).Click();
|
||||
lnurl = new Uri(LNURL.LNURL.Parse(s.Driver.FindElement(By.Id("qr-code-data-input")).GetAttribute("value"), out _).ToString().Replace("https", "http"));
|
||||
s.Driver.FindElement(By.CssSelector("button[data-bs-dismiss='modal']")).Click();
|
||||
var amount = new LightMoney(21021, LightMoneyUnit.Satoshi);
|
||||
info = Assert.IsType<LNURLWithdrawRequest>(await LNURL.LNURL.FetchInformation(lnurl, s.Server.PayTester.HttpClient));
|
||||
Assert.Equal(amount, info.MaxWithdrawable);
|
||||
Assert.Equal(amount, info.CurrentBalance);
|
||||
info = Assert.IsType<LNURLWithdrawRequest>(await LNURL.LNURL.FetchInformation(info.BalanceCheck, s.Server.PayTester.HttpClient));
|
||||
Assert.Equal(amount, info.MaxWithdrawable);
|
||||
Assert.Equal(amount, info.CurrentBalance);
|
||||
|
||||
bolt2 = (await s.Server.CustomerLightningD.CreateInvoice(
|
||||
amount,
|
||||
$"LNurl w payout test {DateTime.UtcNow.Ticks}",
|
||||
TimeSpan.FromHours(1), CancellationToken.None));
|
||||
response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient);
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
Assert.Contains(bolt2.BOLT11, s.Driver.PageSource);
|
||||
|
||||
Assert.Contains(PayoutState.Completed.GetStateString(), s.Driver.PageSource);
|
||||
Assert.Equal(LightningInvoiceStatus.Paid, (await s.Server.CustomerLightningD.GetInvoice(bolt2.Id)).Status);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -2030,6 +2087,145 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
public async Task CanUsePOSKeypad()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
|
||||
s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
s.GoToStore();
|
||||
s.AddLightningNode(LightningConnectionType.CLightning, false);
|
||||
s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click();
|
||||
s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString());
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
TestUtils.Eventually(() => Assert.Contains("App successfully created", s.FindAlertMessage().Text));
|
||||
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Light']")).Click();
|
||||
s.Driver.FindElement(By.Id("Currency")).SendKeys("EUR");
|
||||
s.Driver.FindElement(By.Id("CustomTipPercentages")).Clear();
|
||||
s.Driver.FindElement(By.Id("CustomTipPercentages")).SendKeys("10,21");
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
var windows = s.Driver.WindowHandles;
|
||||
Assert.Equal(2, windows.Count);
|
||||
s.Driver.SwitchTo().Window(windows[1]);
|
||||
s.Driver.WaitForElement(By.ClassName("keypad"));
|
||||
|
||||
// basic checks
|
||||
Assert.Contains("EUR", s.Driver.FindElement(By.Id("Currency")).Text);
|
||||
Assert.Contains("0,00", s.Driver.FindElement(By.Id("Amount")).Text);
|
||||
Assert.Equal("", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-amount")).Selected);
|
||||
Assert.False(s.Driver.FindElement(By.Id("ModeTablist-discount")).Enabled);
|
||||
Assert.False(s.Driver.FindElement(By.Id("ModeTablist-tip")).Enabled);
|
||||
|
||||
// Amount: 1234,56
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='1']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='2']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='3']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='4']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='.']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='5']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='6']")).Click();
|
||||
Assert.Equal("1.234,56", s.Driver.FindElement(By.Id("Amount")).Text);
|
||||
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-discount")).Enabled);
|
||||
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-tip")).Enabled);
|
||||
Assert.Equal("", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
|
||||
// Discount: 10%
|
||||
s.Driver.FindElement(By.CssSelector("label[for='ModeTablist-discount']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='1']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='0']")).Click();
|
||||
Assert.Contains("1.111,10", s.Driver.FindElement(By.Id("Amount")).Text);
|
||||
Assert.Contains("10% discount", s.Driver.FindElement(By.Id("Discount")).Text);
|
||||
Assert.Contains("1.234,56 € - 123,46 € (10%)", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
|
||||
// Tip: 10%
|
||||
s.Driver.FindElement(By.CssSelector("label[for='ModeTablist-tip']")).Click();
|
||||
s.Driver.WaitForElement(By.Id("Tip-Custom"));
|
||||
s.Driver.FindElement(By.Id("Tip-10")).Click();
|
||||
Assert.Contains("1.222,21", s.Driver.FindElement(By.Id("Amount")).Text);
|
||||
Assert.Contains("1.234,56 € - 123,46 € (10%) + 111,11 € (10%)", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
|
||||
// Pay
|
||||
s.Driver.FindElement(By.Id("pay-button")).Click();
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
s.Driver.FindElement(By.Id("DetailsToggle")).Click();
|
||||
s.Driver.WaitForElement(By.Id("PaymentDetails-TotalFiat"));
|
||||
Assert.Contains("1 222,21 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
public async Task CanUsePOSCart()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
|
||||
s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
s.GoToStore();
|
||||
s.AddLightningNode(LightningConnectionType.CLightning, false);
|
||||
s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click();
|
||||
s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString());
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
|
||||
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Cart']")).Click();
|
||||
s.Driver.FindElement(By.Id("Currency")).SendKeys("EUR");
|
||||
s.Driver.FindElement(By.Id("ShowCustomAmount")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
var windows = s.Driver.WindowHandles;
|
||||
Assert.Equal(2, windows.Count);
|
||||
s.Driver.SwitchTo().Window(windows[1]);
|
||||
s.Driver.WaitForElement(By.Id("js-cart-list"));
|
||||
Assert.Empty(s.Driver.FindElements(By.CssSelector("#js-cart-list tbody tr")));
|
||||
Assert.Equal("0,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
Assert.False(s.Driver.FindElement(By.Id("CartClear")).Displayed);
|
||||
|
||||
// Select and clear
|
||||
s.Driver.FindElement(By.CssSelector(".card.js-add-cart:nth-child(1)")).Click();
|
||||
Assert.Single(s.Driver.FindElements(By.CssSelector("#js-cart-list tbody tr")));
|
||||
s.Driver.FindElement(By.Id("CartClear")).Click();
|
||||
Assert.Empty(s.Driver.FindElements(By.CssSelector("#js-cart-list tbody tr")));
|
||||
Thread.Sleep(250);
|
||||
|
||||
// Select items
|
||||
s.Driver.FindElement(By.CssSelector(".card.js-add-cart:nth-child(2)")).Click();
|
||||
Thread.Sleep(250);
|
||||
s.Driver.FindElement(By.CssSelector(".card.js-add-cart:nth-child(1)")).Click();
|
||||
Thread.Sleep(250);
|
||||
Assert.Equal(2, s.Driver.FindElements(By.CssSelector("#js-cart-list tbody tr")).Count);
|
||||
Assert.Equal("2,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
|
||||
// Custom amount
|
||||
s.Driver.FindElement(By.Id("CartCustomAmount")).SendKeys("1.5");
|
||||
s.Driver.FindElement(By.Id("CartTotal")).Click();
|
||||
Assert.Equal("3,50 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
s.Driver.FindElement(By.Id("js-cart-confirm")).Click();
|
||||
|
||||
// Pay
|
||||
Assert.Equal("3,50 €", s.Driver.FindElement(By.Id("CartSummaryTotal")).Text);
|
||||
s.Driver.FindElement(By.Id("js-cart-pay")).Click();
|
||||
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
s.Driver.FindElement(By.Id("DetailsToggle")).Click();
|
||||
s.Driver.WaitForElement(By.Id("PaymentDetails-TotalFiat"));
|
||||
Assert.Contains("3,50 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
@ -2259,7 +2455,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var addresses = s.Driver.FindElements(By.ClassName("lightning-address-value"));
|
||||
Assert.Equal(2, addresses.Count);
|
||||
|
||||
var callbacks = new List<Uri>();
|
||||
foreach (IWebElement webElement in addresses)
|
||||
{
|
||||
var value = webElement.GetAttribute("value");
|
||||
@ -2277,6 +2473,7 @@ namespace BTCPayServer.Tests
|
||||
lnaddress2 = m["text/identifier"];
|
||||
Assert.Equal(2, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi));
|
||||
Assert.Equal(10, request.MaxSendable.ToDecimal(LightMoneyUnit.Satoshi));
|
||||
callbacks.Add(request.Callback);
|
||||
break;
|
||||
|
||||
case { } v when v.StartsWith(lnaddress1):
|
||||
@ -2284,6 +2481,7 @@ namespace BTCPayServer.Tests
|
||||
lnaddress1 = m["text/identifier"];
|
||||
Assert.Equal(1, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi));
|
||||
Assert.Equal(6.12m, request.MaxSendable.ToDecimal(LightMoneyUnit.BTC));
|
||||
callbacks.Add(request.Callback);
|
||||
break;
|
||||
default:
|
||||
Assert.False(true, "Should have matched");
|
||||
@ -2291,7 +2489,19 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
var repo = s.Server.PayTester.GetService<InvoiceRepository>();
|
||||
|
||||
var invoices = await repo.GetInvoices(new InvoiceQuery() { StoreId = new[] { s.StoreId } });
|
||||
// Resolving a ln address shouldn't create any btcpay invoice.
|
||||
// This must be done because some NOST clients resolve ln addresses preemptively without user interaction
|
||||
Assert.Empty(invoices);
|
||||
|
||||
// Calling the callbacks should create the invoices
|
||||
foreach (var callback in callbacks)
|
||||
{
|
||||
using var r = await s.Server.PayTester.HttpClient.GetAsync(callback);
|
||||
await r.Content.ReadAsStringAsync();
|
||||
}
|
||||
invoices = await repo.GetInvoices(new InvoiceQuery() { StoreId = new[] { s.StoreId } });
|
||||
Assert.Equal(2, invoices.Length);
|
||||
var emailSuffix = $"@{s.Server.PayTester.HostName}:{s.Server.PayTester.Port}";
|
||||
foreach (var i in invoices)
|
||||
@ -2460,6 +2670,145 @@ retry:
|
||||
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)
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -290,9 +290,9 @@ retry:
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanGetRateCryptoCurrenciesByDefault()
|
||||
public async Task CanGetRateCryptoCurrenciesByDefault()
|
||||
{
|
||||
string[] brokenShitcoins = { "BTX_USD", "CHC_USD" };
|
||||
string[] brokenShitcoins = { };
|
||||
var provider = new BTCPayNetworkProvider(ChainName.Mainnet);
|
||||
var factory = FastTests.CreateBTCPayRateFactory();
|
||||
var fetcher = new RateFetcher(factory);
|
||||
@ -305,15 +305,37 @@ retry:
|
||||
var result = fetcher.FetchRates(pairs, rules, default);
|
||||
foreach ((CurrencyPair key, Task<RateResult> value) in result)
|
||||
{
|
||||
var rateResult = value.GetAwaiter().GetResult();
|
||||
var rateResult = await value;
|
||||
TestLogs.LogInformation($"Testing {key}");
|
||||
if (brokenShitcoins.Contains(key.ToString()))
|
||||
continue;
|
||||
Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}");
|
||||
}
|
||||
|
||||
var b = new StoreBlob();
|
||||
foreach (var k in StoreBlob.RecommendedExchanges)
|
||||
{
|
||||
b.DefaultCurrency = k.Key;
|
||||
rules = b.GetDefaultRateRules(provider);
|
||||
pairs =
|
||||
provider.GetAll()
|
||||
.Select(c => new CurrencyPair(c.CryptoCode, k.Key))
|
||||
.ToHashSet();
|
||||
result = fetcher.FetchRates(pairs, rules, default);
|
||||
foreach ((CurrencyPair key, Task<RateResult> value) in result)
|
||||
{
|
||||
var rateResult = await value;
|
||||
TestLogs.LogInformation($"Testing {key} when default currency is {k.Key}");
|
||||
if (brokenShitcoins.Contains(key.ToString()))
|
||||
continue;
|
||||
Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public async Task CheckJsContent()
|
||||
{
|
||||
// This test verify that no malicious js is added in the minified files.
|
||||
@ -322,42 +344,63 @@ retry:
|
||||
var actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap", "bootstrap.bundle.min.js").Trim();
|
||||
var version = Regex.Match(actual, "Bootstrap v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
var expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/bootstrap@{version}/dist/js/bootstrap.bundle.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "clipboard.js", "clipboard.js");
|
||||
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vuejs", "vue.min.js").Trim();
|
||||
version = Regex.Match(actual, "Vue\\.js v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdnjs.cloudflare.com/ajax/libs/vue/{version}/vue.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "i18next.min.js").Trim();
|
||||
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/i18next/22.0.6/i18next.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "i18nextHttpBackend.min.js").Trim();
|
||||
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/i18next-http-backend/2.0.1/i18nextHttpBackend.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "vue-i18next.js").Trim();
|
||||
expected = (await (await client.GetAsync("https://unpkg.com/@panter/vue-i18next@0.15.2/dist/vue-i18next.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-qrcode", "vue-qrcode.min.js").Trim();
|
||||
version = Regex.Match(actual, "vue-qrcode v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://unpkg.com/@chenfengyuan/vue-qrcode@{version}/dist/vue-qrcode.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "tom-select", "tom-select.complete.min.js").Trim();
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "dom-confetti", "dom-confetti.min.js").Trim();
|
||||
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);
|
||||
EqualJsContent(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();
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap-vue", "bootstrap-vue.min.js").Trim();
|
||||
version = Regex.Match(actual, "BootstrapVue ([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdnjs.cloudflare.com/ajax/libs/bootstrap-vue/{version}/bootstrap-vue.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-sanitize-directive", "vue-sanitize-directive.umd.min.js").Trim();
|
||||
version = Regex.Match(actual, "Original file: /npm/vue-sanitize-directive@([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/vue-sanitize-directive@{version}/dist/vue-sanitize-directive.umd.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
}
|
||||
|
||||
private void EqualJsContent(string expected, string actual)
|
||||
{
|
||||
if (expected != actual)
|
||||
Assert.Equal(expected, actual.ReplaceLineEndings("\n"));
|
||||
}
|
||||
|
||||
string GetFileContent(params string[] path)
|
||||
|
@ -39,6 +39,7 @@ using BTCPayServer.Plugins.PayButton;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Security.Bitpay;
|
||||
using BTCPayServer.Security.Greenfield;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
@ -718,7 +719,7 @@ namespace BTCPayServer.Tests
|
||||
btcDerivationScheme.GetDerivation(new KeyPath("0/90")).ScriptPubKey, Money.Coins(1.0m));
|
||||
tester.ExplorerNode.Generate(1);
|
||||
var transactions = Assert.IsType<ListTransactionsViewModel>(Assert
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
|
||||
Assert.Empty(transactions.Transactions);
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(walletController.WalletRescan(walletId, rescan).Result);
|
||||
@ -747,7 +748,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotNull(rescan.TimeOfScan);
|
||||
Assert.Equal(1, rescan.LastSuccess.Found);
|
||||
transactions = Assert.IsType<ListTransactionsViewModel>(Assert
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
|
||||
var tx = Assert.Single(transactions.Transactions);
|
||||
Assert.Equal(tx.Id, txId.ToString());
|
||||
|
||||
@ -762,7 +763,7 @@ namespace BTCPayServer.Tests
|
||||
await walletController.ModifyTransaction(walletId, tx.Id, addcomment: "hello"));
|
||||
|
||||
transactions = Assert.IsType<ListTransactionsViewModel>(Assert
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
|
||||
tx = Assert.Single(transactions.Transactions);
|
||||
|
||||
Assert.Equal("hello", tx.Comment);
|
||||
@ -774,7 +775,7 @@ namespace BTCPayServer.Tests
|
||||
await walletController.ModifyTransaction(walletId, tx.Id, removelabel: "test2"));
|
||||
|
||||
transactions = Assert.IsType<ListTransactionsViewModel>(Assert
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
|
||||
tx = Assert.Single(transactions.Transactions);
|
||||
|
||||
Assert.Equal("hello", tx.Comment);
|
||||
@ -1962,7 +1963,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));
|
||||
@ -1983,6 +1985,7 @@ namespace BTCPayServer.Tests
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess(true);
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var btcpayClient = await user.CreateClient();
|
||||
|
||||
DateTimeOffset expiration = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(21);
|
||||
|
||||
@ -2063,6 +2066,20 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var zeroInvoicePM = await greenfield.GetInvoicePaymentMethods(user.StoreId, zeroInvoice.Id);
|
||||
Assert.Empty(zeroInvoicePM);
|
||||
|
||||
var invoice6 = await btcpayClient.CreateInvoice(user.StoreId,
|
||||
new CreateInvoiceRequest()
|
||||
{
|
||||
Amount = GreenfieldConstants.MaxAmount,
|
||||
Currency = "USD"
|
||||
});
|
||||
var repo = tester.PayTester.GetService<InvoiceRepository>();
|
||||
var entity = (await repo.GetInvoice(invoice6.Id));
|
||||
Assert.Equal((decimal)ulong.MaxValue, entity.Price);
|
||||
entity.GetPaymentMethods().First().Calculate();
|
||||
// Shouldn't be possible as we clamp the value, but existing invoice may have that
|
||||
entity.Price = decimal.MaxValue;
|
||||
entity.GetPaymentMethods().First().Calculate();
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
|
@ -73,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"
|
||||
@ -135,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"
|
||||
@ -224,7 +224,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.16.2-beta
|
||||
image: btcpayserver/lnd:v0.16.4-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -259,7 +259,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.16.2-beta
|
||||
image: btcpayserver/lnd:v0.16.4-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -70,7 +70,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"
|
||||
@ -121,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"
|
||||
@ -211,7 +211,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.16.2-beta
|
||||
image: btcpayserver/lnd:v0.16.4-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -248,7 +248,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.16.2-beta
|
||||
image: btcpayserver/lnd:v0.16.4-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.24" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.28" />
|
||||
<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>
|
||||
|
@ -40,7 +40,7 @@ public class AppTopItems : ViewComponent
|
||||
var app = HttpContext.GetAppData();
|
||||
var entries = await _appService.GetItemStats(app);
|
||||
vm.SalesCount = entries.Select(e => e.SalesCount).ToList();
|
||||
vm.Entries = entries.ToList();
|
||||
vm.Entries = entries.Take(5).ToList();
|
||||
vm.AppType = app.AppType;
|
||||
vm.AppUrl = await appBaseType.ConfigureLink(app);
|
||||
vm.Name = app.Name;
|
||||
|
@ -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,15 +23,17 @@
|
||||
@if (Model.Balance.OffchainBalance != null)
|
||||
{
|
||||
<div class="balance">
|
||||
<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 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">
|
||||
<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>
|
||||
@ -42,7 +44,7 @@
|
||||
}
|
||||
@if (Model.Balance.OffchainBalance.Local != null)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<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>
|
||||
@ -53,7 +55,7 @@
|
||||
}
|
||||
@if (Model.Balance.OffchainBalance.Remote != null)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<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>
|
||||
@ -64,7 +66,7 @@
|
||||
}
|
||||
@if (Model.Balance.OffchainBalance.Closing != null)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<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>
|
||||
@ -79,14 +81,16 @@
|
||||
@if (Model.Balance.OnchainBalance != null)
|
||||
{
|
||||
<div class="balance">
|
||||
<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 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">
|
||||
<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>
|
||||
@ -97,7 +101,7 @@
|
||||
}
|
||||
@if (Model.Balance.OnchainBalance.Unconfirmed != null)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<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>
|
||||
@ -108,7 +112,7 @@
|
||||
}
|
||||
@if (Model.Balance.OnchainBalance.Reserved != null)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<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>
|
||||
|
@ -3,6 +3,7 @@
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Services.Invoices
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
|
||||
@model BTCPayServer.Components.StoreRecentInvoices.StoreRecentInvoicesViewModel
|
||||
|
||||
<div class="widget store-recent-invoices" id="StoreRecentInvoices-@Model.Store.Id">
|
||||
@ -51,19 +52,41 @@
|
||||
<a asp-controller="UIInvoice" asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId" class="text-break">@invoice.InvoiceId</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
|
||||
@invoice.Status.Status.ToModernStatus().ToString()
|
||||
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
@if (invoice.Details.Archived)
|
||||
{
|
||||
@($"({invoice.Status.ExceptionStatus.ToString()})")
|
||||
<span class="badge bg-warning">archived</span>
|
||||
}
|
||||
</span>
|
||||
@if (invoice.HasRefund)
|
||||
{
|
||||
<span class="badge bg-warning">
|
||||
Refund
|
||||
<span class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
|
||||
@invoice.Status.Status.ToModernStatus().ToString()
|
||||
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
|
||||
{
|
||||
@($"({invoice.Status.ExceptionStatus.ToString()})")
|
||||
}
|
||||
</span>
|
||||
}
|
||||
@foreach (var paymentMethodId in invoice.Details.Payments.Select(payment => payment.GetPaymentMethodId()).Distinct())
|
||||
{
|
||||
var image = PaymentMethodHandlerDictionary[paymentMethodId]?.GetCryptoImage(paymentMethodId);
|
||||
var badge = paymentMethodId.PaymentType.GetBadge();
|
||||
if (!string.IsNullOrEmpty(image) || !string.IsNullOrEmpty(badge))
|
||||
{
|
||||
<span class="d-inline-flex align-items-center gap-1">
|
||||
@if (!string.IsNullOrEmpty(image))
|
||||
{
|
||||
<img src="@Context.Request.GetRelativePathOrAbsolute(image)" alt="@paymentMethodId.PaymentType.ToString()" style="height:1.5em" />
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(badge))
|
||||
{
|
||||
@badge
|
||||
}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
@if (invoice.HasRefund)
|
||||
{
|
||||
<span class="badge bg-warning">Refund</span>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</span>
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Components.StoreRecentInvoices;
|
||||
@ -11,5 +12,7 @@ public class StoreRecentInvoiceViewModel
|
||||
public string Currency { get; set; }
|
||||
public InvoiceState Status { get; set; }
|
||||
public DateTimeOffset Date { get; set; }
|
||||
|
||||
public InvoiceDetailsModel Details { get; set; }
|
||||
public bool HasRefund { get; set; }
|
||||
}
|
||||
|
@ -3,11 +3,13 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Components.StoreRecentInvoices;
|
||||
|
||||
@ -53,17 +55,22 @@ public class StoreRecentInvoices : ViewComponent
|
||||
});
|
||||
|
||||
vm.Invoices = (from invoice in invoiceEntities
|
||||
let state = invoice.GetInvoiceState()
|
||||
select new StoreRecentInvoiceViewModel
|
||||
{
|
||||
Date = invoice.InvoiceTime,
|
||||
Status = state,
|
||||
HasRefund = invoice.Refunds.Any(),
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.Metadata.OrderId ?? string.Empty,
|
||||
Amount = invoice.Price,
|
||||
Currency = invoice.Currency
|
||||
}).ToList();
|
||||
let state = invoice.GetInvoiceState()
|
||||
select new StoreRecentInvoiceViewModel
|
||||
{
|
||||
Date = invoice.InvoiceTime,
|
||||
Status = state,
|
||||
HasRefund = invoice.Refunds.Any(),
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.Metadata.OrderId ?? string.Empty,
|
||||
Amount = invoice.Price,
|
||||
Currency = invoice.Currency,
|
||||
Details = new InvoiceDetailsModel
|
||||
{
|
||||
Archived = invoice.Archived,
|
||||
Payments = invoice.GetPayments(false)
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ public class StoreRecentTransactions : ViewComponent
|
||||
{
|
||||
var network = derivationSettings.Network;
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
var allTransactions = await wallet.FetchTransactionHistory(derivationSettings.AccountDerivation, 0, 5, TimeSpan.FromDays(31.0));
|
||||
var allTransactions = await wallet.FetchTransactionHistory(derivationSettings.AccountDerivation, 0, 5, TimeSpan.FromDays(31.0), cancellationToken: this.HttpContext.RequestAborted);
|
||||
var walletTransactionsInfo = await _walletRepository.GetWalletTransactionsInfo(vm.WalletId, allTransactions.Select(t => t.TransactionId.ToString()).ToArray());
|
||||
|
||||
transactions = allTransactions
|
||||
|
@ -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,13 +29,10 @@
|
||||
{
|
||||
<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>
|
||||
}
|
||||
@if (Model.Options.Any())
|
||||
{
|
||||
|
@ -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,7 +17,7 @@
|
||||
<header class="mb-3">
|
||||
@if (Model.Balance != null)
|
||||
{
|
||||
<div class="balance">
|
||||
<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 })
|
||||
});
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -12,6 +12,7 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security.Greenfield;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
@ -183,6 +184,10 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Amount), "The amount should be 0 or more.");
|
||||
}
|
||||
if (request.Amount > GreenfieldConstants.MaxAmount)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Amount), $"The amount should less than {GreenfieldConstants.MaxAmount}.");
|
||||
}
|
||||
request.Checkout ??= new CreateInvoiceRequest.CheckoutOptions();
|
||||
if (request.Checkout.PaymentMethods?.Any() is true)
|
||||
{
|
||||
|
@ -255,7 +255,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Email = blob.Email,
|
||||
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts,
|
||||
EmbeddedCSS = blob.EmbeddedCSS,
|
||||
CustomCSSLink = blob.CustomCSSLink
|
||||
CustomCSSLink = blob.CustomCSSLink,
|
||||
FormResponse = blob.FormResponse,
|
||||
FormId = blob.FormId
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -255,16 +255,15 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return PullPaymentNotFound();
|
||||
|
||||
var blob = pp.GetBlob();
|
||||
var pms = blob.SupportedPaymentMethods.FirstOrDefault(id => id.PaymentType == LightningPaymentType.Instance && _networkProvider.DefaultNetwork.CryptoCode == id.CryptoCode);
|
||||
if (pms is not null && blob.Currency.Equals(pms.CryptoCode, StringComparison.InvariantCultureIgnoreCase))
|
||||
if (_pullPaymentService.SupportsLNURL(blob))
|
||||
{
|
||||
var lnurlEndpoint = new Uri(Url.Action("GetLNURLForPullPayment", "UILNURL", new
|
||||
{
|
||||
cryptoCode = _networkProvider.DefaultNetwork.CryptoCode,
|
||||
pullPaymentId = pullPaymentId
|
||||
pullPaymentId
|
||||
}, Request.Scheme, Request.Host.ToString())!);
|
||||
|
||||
return base.Ok(new PullPaymentLNURL()
|
||||
return base.Ok(new PullPaymentLNURL
|
||||
{
|
||||
LNURLBech32 = LNURL.LNURL.EncodeUri(lnurlEndpoint, "withdrawRequest", true).ToString(),
|
||||
LNURLUri = LNURL.LNURL.EncodeUri(lnurlEndpoint, "withdrawRequest", false).ToString()
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
@ -182,7 +182,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[FromQuery] TransactionStatus[]? statusFilter = null,
|
||||
[FromQuery] string? labelFilter = null,
|
||||
[FromQuery] int skip = 0,
|
||||
[FromQuery] int limit = int.MaxValue
|
||||
[FromQuery] int limit = int.MaxValue,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out var network,
|
||||
@ -197,7 +198,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
if (statusFilter?.Any() is true || !string.IsNullOrWhiteSpace(labelFilter))
|
||||
preFiltering = false;
|
||||
var txs = await wallet.FetchTransactionHistory(derivationScheme.AccountDerivation, preFiltering ? skip : 0,
|
||||
preFiltering ? limit : int.MaxValue);
|
||||
preFiltering ? limit : int.MaxValue, cancellationToken: cancellationToken);
|
||||
if (!preFiltering)
|
||||
{
|
||||
var filteredList = new List<TransactionHistoryLine>(txs.Count);
|
||||
@ -585,6 +586,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");
|
||||
}
|
||||
|
||||
|
@ -129,6 +129,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
//we do not include EmailSettings in this model and instead opt to set it in stores/storeid/email endpoints
|
||||
//we do not include PaymentMethodCriteria because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
|
||||
NetworkFeeMode = storeBlob.NetworkFeeMode,
|
||||
DefaultCurrency = storeBlob.DefaultCurrency,
|
||||
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
||||
CheckoutType = storeBlob.CheckoutType,
|
||||
Receipt = InvoiceDataBase.ReceiptOptions.Merge(storeBlob.ReceiptOptions, null),
|
||||
|
@ -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,6 +1,9 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
@ -8,6 +11,7 @@ using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
@ -21,17 +25,20 @@ namespace BTCPayServer.Controllers
|
||||
public UIAppsController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
StoreRepository storeRepository,
|
||||
IFileService fileService,
|
||||
AppService appService,
|
||||
IHtmlHelper html)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_storeRepository = storeRepository;
|
||||
_fileService = fileService;
|
||||
_appService = appService;
|
||||
Html = html;
|
||||
}
|
||||
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly AppService _appService;
|
||||
|
||||
public string CreatedAppId { get; set; }
|
||||
@ -184,13 +191,50 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId = app.StoreDataId });
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpPost("{appId}/upload-file")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> FileUpload(IFormFile file)
|
||||
{
|
||||
var app = GetCurrentApp();
|
||||
var userId = GetUserId();
|
||||
if (app is null || userId is null)
|
||||
return NotFound();
|
||||
|
||||
if (!file.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
|
||||
{
|
||||
return Json(new { error = "The file needs to be an image" });
|
||||
}
|
||||
if (file.Length > 500_000)
|
||||
{
|
||||
return Json(new { error = "The image file size should be less than 0.5MB" });
|
||||
}
|
||||
var formFile = await file.Bufferize();
|
||||
if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))
|
||||
{
|
||||
return Json(new { error = "The file needs to be an image" });
|
||||
}
|
||||
try
|
||||
{
|
||||
var storedFile = await _fileService.AddFile(file, userId);
|
||||
var fileId = storedFile.Id;
|
||||
var fileUrl = await _fileService.GetFileUrl(Request.GetAbsoluteRootUri(), fileId);
|
||||
return Json(new { fileId, fileUrl });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Json(new { error = $"Could not save file: {e.Message}" });
|
||||
}
|
||||
}
|
||||
|
||||
async Task<string> GetStoreDefaultCurrentIfEmpty(string storeId, string currency)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(currency))
|
||||
{
|
||||
currency = (await _storeRepository.FindStore(storeId)).GetStoreBlob().DefaultCurrency;
|
||||
var store = await _storeRepository.FindStore(storeId);
|
||||
currency = store?.GetStoreBlob().DefaultCurrency;
|
||||
}
|
||||
return currency.Trim().ToUpperInvariant();
|
||||
return currency?.Trim().ToUpperInvariant();
|
||||
}
|
||||
|
||||
private string GetUserId() => _userManager.GetUserId(User);
|
||||
|
@ -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,13 +79,13 @@ 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);
|
||||
return stores.Any()
|
||||
? RedirectToStore(stores.First())
|
||||
? RedirectToStore(userId, stores.First())
|
||||
: RedirectToAction(nameof(UIUserStoresController.CreateStore), "UIUserStores");
|
||||
}
|
||||
|
||||
@ -211,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 });
|
||||
}
|
||||
|
@ -172,7 +172,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("i/{invoiceId}/receipt")]
|
||||
public async Task<IActionResult> InvoiceReceipt(string invoiceId)
|
||||
public async Task<IActionResult> InvoiceReceipt(string invoiceId, [FromQuery] bool print = false)
|
||||
{
|
||||
var i = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
if (i is null)
|
||||
@ -255,7 +255,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.Payments = receipt.ShowPayments is false ? null : payments;
|
||||
vm.AdditionalData = PosDataParser.ParsePosData(receiptData);
|
||||
|
||||
return View(vm);
|
||||
return View(print ? "InvoiceReceiptPrint" : "InvoiceReceipt", vm);
|
||||
}
|
||||
|
||||
private string? GetTransactionLink(PaymentMethodId paymentMethodId, string txId)
|
||||
@ -456,7 +456,7 @@ namespace BTCPayServer.Controllers
|
||||
model.Title = "How much to refund?";
|
||||
model.RefundStep = RefundSteps.SelectRate;
|
||||
|
||||
if (isPaidOver)
|
||||
if (!isPaidOver)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption), "Invoice is not overpaid");
|
||||
}
|
||||
@ -466,7 +466,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return this.CreateValidationError(ModelState);
|
||||
return View("_RefundModal", model);
|
||||
}
|
||||
|
||||
createPullPayment.Currency = paymentMethodId.CryptoCode;
|
||||
@ -638,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;
|
||||
|
@ -17,6 +17,7 @@ using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Security.Greenfield;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
@ -127,6 +128,14 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
throw new BitpayHttpException(400, "The expirationTime is set too soon");
|
||||
}
|
||||
if (entity.Price < 0.0m)
|
||||
{
|
||||
throw new BitpayHttpException(400, "The price should be 0 or more.");
|
||||
}
|
||||
if (entity.Price > GreenfieldConstants.MaxAmount)
|
||||
{
|
||||
throw new BitpayHttpException(400, $"The price should less than {GreenfieldConstants.MaxAmount}.");
|
||||
}
|
||||
entity.Metadata.OrderId = invoice.OrderId;
|
||||
entity.Metadata.PosDataLegacy = invoice.PosData;
|
||||
entity.ServerUrl = serverUrl;
|
||||
@ -278,6 +287,7 @@ namespace BTCPayServer.Controllers
|
||||
if (string.IsNullOrEmpty(entity.Currency))
|
||||
entity.Currency = storeBlob.DefaultCurrency;
|
||||
entity.Currency = entity.Currency.Trim().ToUpperInvariant();
|
||||
entity.Price = Math.Min(GreenfieldConstants.MaxAmount, entity.Price);
|
||||
entity.Price = Math.Max(0.0m, entity.Price);
|
||||
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(entity.Currency, false);
|
||||
if (currencyInfo != null)
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
@ -109,24 +110,24 @@ namespace BTCPayServer
|
||||
}
|
||||
|
||||
var blob = pp.GetBlob();
|
||||
if (!blob.Currency.Equals(cryptoCode, StringComparison.InvariantCultureIgnoreCase))
|
||||
if (!_pullPaymentHostedService.SupportsLNURL(blob))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var unit = blob.Currency == "SATS" ? LightMoneyUnit.Satoshi : LightMoneyUnit.BTC;
|
||||
var progress = _pullPaymentHostedService.CalculatePullPaymentProgress(pp, DateTimeOffset.UtcNow);
|
||||
|
||||
var remaining = progress.Limit - progress.Completed - progress.Awaiting;
|
||||
var request = new LNURLWithdrawRequest
|
||||
{
|
||||
MaxWithdrawable = LightMoney.FromUnit(remaining, LightMoneyUnit.BTC),
|
||||
MaxWithdrawable = LightMoney.FromUnit(remaining, unit),
|
||||
K1 = pullPaymentId,
|
||||
BalanceCheck = new Uri(Request.GetCurrentUrl()),
|
||||
CurrentBalance = LightMoney.FromUnit(remaining, LightMoneyUnit.BTC),
|
||||
CurrentBalance = LightMoney.FromUnit(remaining, unit),
|
||||
MinWithdrawable =
|
||||
LightMoney.FromUnit(
|
||||
Math.Min(await _lightningLikePayoutHandler.GetMinimumPayoutAmount(pmi, null), remaining),
|
||||
LightMoneyUnit.BTC),
|
||||
unit),
|
||||
Tag = "withdrawRequest",
|
||||
Callback = new Uri(Request.GetCurrentUrl()),
|
||||
// It's not `pp.GetBlob().Description` because this would be HTML
|
||||
@ -154,13 +155,13 @@ namespace BTCPayServer
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var claimResponse = await _pullPaymentHostedService.Claim(new ClaimRequest()
|
||||
var claimResponse = await _pullPaymentHostedService.Claim(new ClaimRequest
|
||||
{
|
||||
Destination = new BoltInvoiceClaimDestination(pr, result),
|
||||
PaymentMethodId = pmi,
|
||||
PullPaymentId = pullPaymentId,
|
||||
StoreId = pp.StoreId,
|
||||
Value = result.MinimumAmount.ToDecimal(LightMoneyUnit.BTC)
|
||||
Value = result.MinimumAmount.ToDecimal(unit)
|
||||
});
|
||||
|
||||
if (claimResponse.Result != ClaimRequest.ClaimResult.Ok)
|
||||
@ -257,12 +258,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
|
||||
@ -295,7 +296,8 @@ namespace BTCPayServer
|
||||
|
||||
var createInvoice = new CreateInvoiceRequest()
|
||||
{
|
||||
Amount = item?.Price.Value,
|
||||
Amount = item?.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup? null: item?.Price,
|
||||
|
||||
Currency = currencyCode,
|
||||
Checkout = new InvoiceDataBase.CheckoutOptions()
|
||||
{
|
||||
@ -373,13 +375,52 @@ namespace BTCPayServer
|
||||
return NotFound("Unknown username");
|
||||
|
||||
var store = await _storeRepository.FindStore(lightningAddressSettings.StoreDataId);
|
||||
var cryptoCode = "BTC";
|
||||
if (store is null)
|
||||
return NotFound("Unknown username");
|
||||
if (GetLNUrlPaymentMethodId(cryptoCode, store, out var lnUrlMethod) is null)
|
||||
return NotFound("LNUrl not available for store");
|
||||
|
||||
var blob = lightningAddressSettings.GetBlob();
|
||||
|
||||
return await GetLNURLRequest(
|
||||
"BTC",
|
||||
var lnurlRequest = new LNURLPayRequest()
|
||||
{
|
||||
Tag = "payRequest",
|
||||
MinSendable = blob?.Min is decimal min ? new LightMoney(min, LightMoneyUnit.Satoshi) : null,
|
||||
MaxSendable = blob?.Max is decimal max ? new LightMoney(max, LightMoneyUnit.Satoshi) : null,
|
||||
CommentAllowed = lnUrlMethod.LUD12Enabled ? 2000 : 0
|
||||
};
|
||||
NormalizeSendable(lnurlRequest);
|
||||
|
||||
var lnUrlMetadata = new Dictionary<string, string>()
|
||||
{
|
||||
["text/identifier"] = $"{username}@{Request.Host}"
|
||||
};
|
||||
SetLNUrlDescriptionMetadata(lnUrlMetadata, store, store.GetStoreBlob(), null);
|
||||
lnurlRequest.Metadata =
|
||||
JsonConvert.SerializeObject(lnUrlMetadata.Select(kv => new[] { kv.Key, kv.Value }));
|
||||
|
||||
lnurlRequest.Callback = new Uri(_linkGenerator.GetUriByAction(
|
||||
action: nameof(GetLNURLForLightningAddress),
|
||||
controller: "UILNURL",
|
||||
values: new { cryptoCode, username }, Request.Scheme, Request.Host, Request.PathBase));
|
||||
|
||||
lnurlRequest = await _pluginHookService.ApplyFilter("modify-lnurlp-request", lnurlRequest) as LNURLPayRequest;
|
||||
return Ok(lnurlRequest);
|
||||
}
|
||||
|
||||
[HttpGet("pay/lnaddress/{username}")]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> GetLNURLForLightningAddress(string cryptoCode, string username, [FromQuery] long? amount = null, string comment = null)
|
||||
{
|
||||
var lightningAddressSettings = await _lightningAddressService.ResolveByAddress(username);
|
||||
if (lightningAddressSettings is null || username is null)
|
||||
return NotFound("Unknown username");
|
||||
var blob = lightningAddressSettings.GetBlob();
|
||||
var store = await _storeRepository.FindStore(lightningAddressSettings.StoreDataId);
|
||||
var result = await GetLNURLRequest(
|
||||
cryptoCode,
|
||||
store,
|
||||
store.GetStoreBlob(),
|
||||
new CreateInvoiceRequest()
|
||||
@ -396,31 +437,44 @@ namespace BTCPayServer
|
||||
{
|
||||
{ "text/identifier", $"{username}@{Request.Host}" }
|
||||
});
|
||||
if (result is not OkObjectResult ok || ok.Value is not LNURLPayRequest payRequest)
|
||||
return result;
|
||||
var invoiceId = payRequest.Callback.AbsoluteUri.Split('/').Last();
|
||||
return await GetLNURLForInvoice(invoiceId, cryptoCode, amount, comment);
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("pay")]
|
||||
[HttpGet("{storeId}/pay")]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> GetLNUrlForStore(
|
||||
string cryptoCode,
|
||||
string storeId,
|
||||
string currencyCode = null)
|
||||
string currency = null,
|
||||
string orderId = null,
|
||||
decimal? amount = null)
|
||||
{
|
||||
var store = this.HttpContext.GetStoreData();
|
||||
var store = await _storeRepository.FindStore(storeId);
|
||||
if (store is null)
|
||||
return NotFound();
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
var blob = store.GetStoreBlob();
|
||||
if (!blob.AnyoneCanInvoice)
|
||||
return NotFound("'Anyone can invoice' is turned off");
|
||||
var metadata = new InvoiceMetadata();
|
||||
if (!string.IsNullOrEmpty(orderId))
|
||||
{
|
||||
metadata.OrderId = orderId;
|
||||
}
|
||||
return await GetLNURLRequest(
|
||||
cryptoCode,
|
||||
store,
|
||||
blob,
|
||||
new CreateInvoiceRequest
|
||||
{
|
||||
Currency = currencyCode
|
||||
Amount = amount,
|
||||
Metadata = metadata.ToJObject(),
|
||||
Currency = currency
|
||||
});
|
||||
}
|
||||
|
||||
@ -482,11 +536,7 @@ namespace BTCPayServer
|
||||
|
||||
if (!lnUrlMetadata.ContainsKey("text/plain"))
|
||||
{
|
||||
var invoiceDescription = blob.LightningDescriptionTemplate
|
||||
.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{ItemDescription}", i.Metadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{OrderId}", i.Metadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
|
||||
lnUrlMetadata.Add("text/plain", invoiceDescription);
|
||||
SetLNUrlDescriptionMetadata(lnUrlMetadata, store, blob, i.Metadata);
|
||||
}
|
||||
|
||||
lnurlRequest.Tag = "payRequest";
|
||||
@ -503,12 +553,7 @@ namespace BTCPayServer
|
||||
lnurlRequest.MaxSendable = lnurlRequest.MinSendable;
|
||||
}
|
||||
|
||||
// We don't think BTCPay handle well 0 sats payments, just in case make it minimum one sat.
|
||||
if (lnurlRequest.MinSendable is null || lnurlRequest.MinSendable < LightMoney.Satoshis(1.0m))
|
||||
lnurlRequest.MinSendable = LightMoney.Satoshis(1.0m);
|
||||
|
||||
if (lnurlRequest.MaxSendable is null)
|
||||
lnurlRequest.MaxSendable = LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC);
|
||||
NormalizeSendable(lnurlRequest);
|
||||
|
||||
lnurlRequest = await _pluginHookService.ApplyFilter("modify-lnurlp-request", lnurlRequest) as LNURLPayRequest;
|
||||
if (paymentMethodDetails.PayRequest is null)
|
||||
@ -524,6 +569,25 @@ namespace BTCPayServer
|
||||
return lnurlRequest;
|
||||
}
|
||||
|
||||
private void SetLNUrlDescriptionMetadata(Dictionary<string, string> lnUrlMetadata, Data.StoreData store, StoreBlob blob, InvoiceMetadata invoiceMetadata)
|
||||
{
|
||||
var invoiceDescription = blob.LightningDescriptionTemplate
|
||||
.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{ItemDescription}", invoiceMetadata?.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{OrderId}", invoiceMetadata?.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
|
||||
lnUrlMetadata.Add("text/plain", invoiceDescription);
|
||||
}
|
||||
|
||||
private static void NormalizeSendable(LNURLPayRequest lnurlRequest)
|
||||
{
|
||||
// We don't think BTCPay handle well 0 sats payments, just in case make it minimum one sat.
|
||||
if (lnurlRequest.MinSendable is null || lnurlRequest.MinSendable < LightMoney.Satoshis(1.0m))
|
||||
lnurlRequest.MinSendable = LightMoney.Satoshis(1.0m);
|
||||
|
||||
if (lnurlRequest.MaxSendable is null)
|
||||
lnurlRequest.MaxSendable = LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC);
|
||||
}
|
||||
|
||||
PaymentMethodId GetLNUrlPaymentMethodId(string cryptoCode, Data.StoreData store, out LNURLPaySupportedPaymentMethod lnUrlSettings)
|
||||
{
|
||||
lnUrlSettings = null;
|
||||
|
@ -506,57 +506,57 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
public static readonly Dictionary<string, (string Title, string Description)> PermissionDescriptions = new Dictionary<string, (string Title, string Description)>()
|
||||
{
|
||||
{Policies.Unrestricted, ("Unrestricted access", "The app will have unrestricted access to your account.")},
|
||||
{Policies.CanViewUsers, ("View users", "The app will be able to see all users on this server.")},
|
||||
{Policies.CanCreateUser, ("Create new users", "The app will be able to create new users on this server.")},
|
||||
{Policies.CanManageUsers, ("Manage users", "The app will be able to create/delete API keys for users.")},
|
||||
{Policies.CanDeleteUser, ("Delete user", "The app will be able to delete the user to whom it is assigned. Admin users can delete any user without this permission.")},
|
||||
{Policies.CanModifyStoreSettings, ("Modify your stores", "The app will be able to manage invoices on all your stores and modify their settings.")},
|
||||
{$"{Policies.CanModifyStoreSettings}:", ("Manage selected stores", "The app will be able to manage invoices on the selected stores and modify their settings.")},
|
||||
{Policies.CanViewCustodianAccounts, ("View exchange accounts linked to your stores", "The app will be able to see exchange accounts linked to your stores.")},
|
||||
{$"{Policies.CanViewCustodianAccounts}:", ("View exchange accounts linked to selected stores", "The app will be able to see exchange accounts linked to the selected stores.")},
|
||||
{Policies.CanManageCustodianAccounts, ("Manage exchange accounts linked to your stores", "The app will be able to modify exchange accounts linked to your stores.")},
|
||||
{$"{Policies.CanManageCustodianAccounts}:", ("Manage exchange accounts linked to selected stores", "The app will be able to modify exchange accounts linked to selected stores.")},
|
||||
{Policies.CanDepositToCustodianAccounts, ("Deposit funds to exchange accounts linked to your stores", "The app will be able to deposit funds to your exchange accounts.")},
|
||||
{$"{Policies.CanDepositToCustodianAccounts}:", ("Deposit funds to exchange accounts linked to selected stores", "The app will be able to deposit funds to selected store's exchange accounts.")},
|
||||
{Policies.CanWithdrawFromCustodianAccounts, ("Withdraw funds from exchange accounts to your store", "The app will be able to withdraw funds from your exchange accounts to your store.")},
|
||||
{$"{Policies.CanWithdrawFromCustodianAccounts}:", ("Withdraw funds from selected store's exchange accounts", "The app will be able to withdraw funds from your selected store's exchange accounts.")},
|
||||
{Policies.CanTradeCustodianAccount, ("Trade funds on your store's exchange accounts", "The app will be able to trade funds on your store's exchange accounts.")},
|
||||
{$"{Policies.CanTradeCustodianAccount}:", ("Trade funds on selected store's exchange accounts", "The app will be able to trade funds on selected store's exchange accounts.")},
|
||||
{Policies.CanModifyStoreWebhooks, ("Modify stores webhooks", "The app will modify the webhooks of all your stores.")},
|
||||
{$"{Policies.CanModifyStoreWebhooks}:", ("Modify selected stores' webhooks", "The app will modify the webhooks of the selected stores.")},
|
||||
{Policies.CanViewStoreSettings, ("View your stores", "The app will be able to view stores settings.")},
|
||||
{$"{Policies.CanViewStoreSettings}:", ("View your stores", "The app will be able to view the selected stores' settings.")},
|
||||
{Policies.CanModifyServerSettings, ("Manage your server", "The app will have total control on the server settings of your server.")},
|
||||
{Policies.CanViewProfile, ("View your profile", "The app will be able to view your user profile.")},
|
||||
{Policies.CanModifyProfile, ("Manage your profile", "The app will be able to view and modify your user profile.")},
|
||||
{Policies.CanManageNotificationsForUser, ("Manage your notifications", "The app will be able to view and modify your user notifications.")},
|
||||
{Policies.CanViewNotificationsForUser, ("View your notifications", "The app will be able to view your user notifications.")},
|
||||
{Policies.CanCreateInvoice, ("Create an invoice", "The app will be able to create new invoices.")},
|
||||
{$"{Policies.CanCreateInvoice}:", ("Create an invoice", "The app will be able to create new invoices on the selected stores.")},
|
||||
{Policies.CanViewInvoices, ("View invoices", "The app will be able to view invoices.")},
|
||||
{Policies.CanModifyInvoices, ("Modify invoices", "The app will be able to modify and view invoices.")},
|
||||
{$"{Policies.CanViewInvoices}:", ("View invoices", "The app will be able to view invoices on the selected stores.")},
|
||||
{$"{Policies.CanModifyInvoices}:", ("Modify invoices", "The app will be able to modify and view invoices on the selected stores.")},
|
||||
{Policies.CanModifyPaymentRequests, ("Modify your payment requests", "The app will be able to view, modify, delete and create new payment requests on all your stores.")},
|
||||
{$"{Policies.CanModifyPaymentRequests}:", ("Manage selected stores' payment requests", "The app will be able to view, modify, delete and create new payment requests on the selected stores.")},
|
||||
{Policies.CanViewPaymentRequests, ("View your payment requests", "The app will be able to view payment requests.")},
|
||||
{$"{Policies.CanViewPaymentRequests}:", ("View your payment requests", "The app will be able to view the selected stores' payment requests.")},
|
||||
{Policies.CanManagePullPayments, ("Manage your pull payments", "The app will be able to view, modify, delete and create pull payments on all your stores.")},
|
||||
{$"{Policies.CanManagePullPayments}:", ("Manage selected stores' pull payments", "The app will be able to view, modify, delete and create new pull payments on the selected stores.")},
|
||||
{Policies.CanCreatePullPayments, ("Create pull payments", "The app will be able to create pull payments on all your stores.")},
|
||||
{$"{Policies.CanCreatePullPayments}:", ("Create pull payments in selected stores", "The app will be able to create new pull payments on the selected stores.")},
|
||||
{Policies.CanCreateNonApprovedPullPayments, ("Create non-approved pull payments", "The app will be able to create pull payments without automatic approval on all your stores.")},
|
||||
{$"{Policies.CanCreateNonApprovedPullPayments}:", ("Create non-approved pull payments in selected stores", "The app will be able to view, modify, delete and create pull payments without automatic approval on the selected stores.")},
|
||||
{Policies.CanUseInternalLightningNode, ("Use the internal lightning node", "The app will be able to use the internal BTCPay Server lightning node to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices.")},
|
||||
{Policies.CanViewLightningInvoiceInternalNode, ("View invoices from internal lightning node", "The app will be able to use the internal BTCPay Server lightning node to view BOLT11 invoices.")},
|
||||
{Policies.CanCreateLightningInvoiceInternalNode, ("Create invoices with internal lightning node", "The app will be able to use the internal BTCPay Server lightning node to create BOLT11 invoices.")},
|
||||
{Policies.CanUseLightningNodeInStore, ("Use the lightning nodes associated with your stores", "The app will be able to use the lightning nodes connected to all your stores to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices.")},
|
||||
{Policies.CanViewLightningInvoiceInStore, ("View the lightning invoices associated with your stores", "The app will be able to view the lightning invoices connected to all your stores.")},
|
||||
{Policies.CanCreateLightningInvoiceInStore, ("Create invoices from the lightning nodes associated with your stores", "The app will be able to use the lightning nodes connected to all your stores to create BOLT11 invoices.")},
|
||||
{$"{Policies.CanUseLightningNodeInStore}:", ("Use the lightning nodes associated with your stores", "The app will be able to use the lightning nodes connected to the selected stores to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices.")},
|
||||
{$"{Policies.CanViewLightningInvoiceInStore}:", ("View the lightning invoices associated with your stores", "The app will be able to view the lightning invoices connected to the selected stores.")},
|
||||
{$"{Policies.CanCreateLightningInvoiceInStore}:", ("Create invoices from the lightning nodes associated with your stores", "The app will be able to use the lightning nodes connected to the selected stores to create BOLT11 invoices.")},
|
||||
{Policies.Unrestricted, ("Unrestricted access", "Grants unrestricted access to your account.")},
|
||||
{Policies.CanViewUsers, ("View users", "Allows seeing all users on this server.")},
|
||||
{Policies.CanCreateUser, ("Create new users", "Allows creating new users on this server.")},
|
||||
{Policies.CanManageUsers, ("Manage users", "Allows creating/deleting API keys for users.")},
|
||||
{Policies.CanDeleteUser, ("Delete user", "Allows deleting the user to whom it is assigned. Admin users can delete any user without this permission.")},
|
||||
{Policies.CanModifyStoreSettings, ("Modify your stores", "Allows managing invoices on all your stores and modify their settings.")},
|
||||
{$"{Policies.CanModifyStoreSettings}:", ("Manage selected stores", "Allows managing invoices on the selected stores and modify their settings.")},
|
||||
{Policies.CanViewCustodianAccounts, ("View exchange accounts linked to your stores", "Allows seeing exchange accounts linked to your stores.")},
|
||||
{$"{Policies.CanViewCustodianAccounts}:", ("View exchange accounts linked to selected stores", "Allows seeing exchange accounts linked to the selected stores.")},
|
||||
{Policies.CanManageCustodianAccounts, ("Manage exchange accounts linked to your stores", "Allows modifying exchange accounts linked to your stores.")},
|
||||
{$"{Policies.CanManageCustodianAccounts}:", ("Manage exchange accounts linked to selected stores", "Allows modifying exchange accounts linked to selected stores.")},
|
||||
{Policies.CanDepositToCustodianAccounts, ("Deposit funds to exchange accounts linked to your stores", "Allows depositing funds to your exchange accounts.")},
|
||||
{$"{Policies.CanDepositToCustodianAccounts}:", ("Deposit funds to exchange accounts linked to selected stores", "Allows depositing funds to selected store's exchange accounts.")},
|
||||
{Policies.CanWithdrawFromCustodianAccounts, ("Withdraw funds from exchange accounts to your store", "Allows withdrawing funds from your exchange accounts to your store.")},
|
||||
{$"{Policies.CanWithdrawFromCustodianAccounts}:", ("Withdraw funds from selected store's exchange accounts", "Allows withdrawing funds from your selected store's exchange accounts.")},
|
||||
{Policies.CanTradeCustodianAccount, ("Trade funds on your store's exchange accounts", "Allows trading funds on your store's exchange accounts.")},
|
||||
{$"{Policies.CanTradeCustodianAccount}:", ("Trade funds on selected store's exchange accounts", "Allows trading funds on selected store's exchange accounts.")},
|
||||
{Policies.CanModifyStoreWebhooks, ("Modify stores webhooks", "Allows modifying the webhooks of all your stores.")},
|
||||
{$"{Policies.CanModifyStoreWebhooks}:", ("Modify selected stores' webhooks", "Allows modifying the webhooks of the selected stores.")},
|
||||
{Policies.CanViewStoreSettings, ("View your stores", "Allows viewing stores settings.")},
|
||||
{$"{Policies.CanViewStoreSettings}:", ("View your stores", "Allows viewing the selected stores' settings.")},
|
||||
{Policies.CanModifyServerSettings, ("Manage your server", "Grants total control on the server settings of your server.")},
|
||||
{Policies.CanViewProfile, ("View your profile", "Allows viewing your user profile.")},
|
||||
{Policies.CanModifyProfile, ("Manage your profile", "Allows viewing and modifying your user profile.")},
|
||||
{Policies.CanManageNotificationsForUser, ("Manage your notifications", "Allows viewing and modifying your user notifications.")},
|
||||
{Policies.CanViewNotificationsForUser, ("View your notifications", "Allows viewing your user notifications.")},
|
||||
{Policies.CanCreateInvoice, ("Create an invoice", "Allows creating new invoices.")},
|
||||
{$"{Policies.CanCreateInvoice}:", ("Create an invoice", "Allows creating new invoices on the selected stores.")},
|
||||
{Policies.CanViewInvoices, ("View invoices", "Allows viewing invoices.")},
|
||||
{Policies.CanModifyInvoices, ("Modify invoices", "Allows viewing and modifying invoices.")},
|
||||
{$"{Policies.CanViewInvoices}:", ("View invoices", "Allows viewing invoices on the selected stores.")},
|
||||
{$"{Policies.CanModifyInvoices}:", ("Modify invoices", "Allows viewing and modifying invoices on the selected stores.")},
|
||||
{Policies.CanModifyPaymentRequests, ("Modify your payment requests", "Allows viewing, modifying, deleting and creating new payment requests on all your stores.")},
|
||||
{$"{Policies.CanModifyPaymentRequests}:", ("Manage selected stores' payment requests", "Allows viewing, modifying, deleting and creating new payment requests on the selected stores.")},
|
||||
{Policies.CanViewPaymentRequests, ("View your payment requests", "Allows viewing payment requests.")},
|
||||
{$"{Policies.CanViewPaymentRequests}:", ("View your payment requests", "Allows viewing the selected stores' payment requests.")},
|
||||
{Policies.CanManagePullPayments, ("Manage your pull payments", "Allows viewing, modifying, deleting and creating pull payments on all your stores.")},
|
||||
{$"{Policies.CanManagePullPayments}:", ("Manage selected stores' pull payments", "Allows viewing, modifying, deleting and creating pull payments on the selected stores.")},
|
||||
{Policies.CanCreatePullPayments, ("Create pull payments", "Allows creating pull payments on all your stores.")},
|
||||
{$"{Policies.CanCreatePullPayments}:", ("Create pull payments in selected stores", "Allows creating pull payments on the selected stores.")},
|
||||
{Policies.CanCreateNonApprovedPullPayments, ("Create non-approved pull payments", "Allows creating pull payments without automatic approval on all your stores.")},
|
||||
{$"{Policies.CanCreateNonApprovedPullPayments}:", ("Create non-approved pull payments in selected stores", "Allows viewing, modifying, deleting and creating pull payments without automatic approval on the selected stores.")},
|
||||
{Policies.CanUseInternalLightningNode, ("Use the internal lightning node", "Allows using the internal BTCPay Server lightning node to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices.")},
|
||||
{Policies.CanViewLightningInvoiceInternalNode, ("View invoices from internal lightning node", "Allows using the internal BTCPay Server lightning node to view BOLT11 invoices.")},
|
||||
{Policies.CanCreateLightningInvoiceInternalNode, ("Create invoices with internal lightning node", "Allows using the internal BTCPay Server lightning node to create BOLT11 invoices.")},
|
||||
{Policies.CanUseLightningNodeInStore, ("Use the lightning nodes associated with your stores", "Allows using the lightning nodes connected to all your stores to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices.")},
|
||||
{Policies.CanViewLightningInvoiceInStore, ("View the lightning invoices associated with your stores", "Allows viewing the lightning invoices connected to all your stores.")},
|
||||
{Policies.CanCreateLightningInvoiceInStore, ("Create invoices from the lightning nodes associated with your stores", "Allows using the lightning nodes connected to all your stores to create BOLT11 invoices.")},
|
||||
{$"{Policies.CanUseLightningNodeInStore}:", ("Use the lightning nodes associated with your stores", "Allows using the lightning nodes connected to the selected stores to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices.")},
|
||||
{$"{Policies.CanViewLightningInvoiceInStore}:", ("View the lightning invoices associated with your stores", "Allows viewing the lightning invoices connected to the selected stores.")},
|
||||
{$"{Policies.CanCreateLightningInvoiceInStore}:", ("Create invoices from the lightning nodes associated with your stores", "Allows using the lightning nodes connected to the selected stores to create BOLT11 invoices.")},
|
||||
};
|
||||
public string Title
|
||||
{
|
||||
|
@ -78,16 +78,20 @@ namespace BTCPayServer.Controllers
|
||||
model = this.ParseListQuery(model ?? new ListPaymentRequestsViewModel());
|
||||
|
||||
var store = GetCurrentStore();
|
||||
var includeArchived = new SearchString(model.SearchTerm, model.TimezoneOffset ?? 0).GetFilterBool("includearchived") == true;
|
||||
var fs = new SearchString(model.SearchTerm, model.TimezoneOffset ?? 0);
|
||||
var result = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery
|
||||
{
|
||||
UserId = GetUserId(),
|
||||
StoreId = store.Id,
|
||||
Skip = model.Skip,
|
||||
Count = model.Count,
|
||||
IncludeArchived = includeArchived
|
||||
Status = fs.GetFilterArray("status")?.Select(s => Enum.Parse<Client.Models.PaymentRequestData.PaymentRequestStatus>(s, true)).ToArray(),
|
||||
IncludeArchived = fs.GetFilterBool("includearchived") ?? false
|
||||
});
|
||||
|
||||
|
||||
model.Search = fs;
|
||||
model.SearchText = fs.TextSearch;
|
||||
|
||||
model.Items = result.Select(data =>
|
||||
{
|
||||
var blob = data.GetBlob();
|
||||
|
@ -29,6 +29,7 @@ namespace BTCPayServer.Controllers
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly DisplayFormatter _displayFormatter;
|
||||
private readonly PullPaymentHostedService _pullPaymentHostedService;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
|
||||
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
@ -37,6 +38,7 @@ namespace BTCPayServer.Controllers
|
||||
CurrencyNameTable currencyNameTable,
|
||||
DisplayFormatter displayFormatter,
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
BTCPayNetworkJsonSerializerSettings serializerSettings,
|
||||
IEnumerable<IPayoutHandler> payoutHandlers,
|
||||
StoreRepository storeRepository)
|
||||
@ -48,6 +50,7 @@ namespace BTCPayServer.Controllers
|
||||
_serializerSettings = serializerSettings;
|
||||
_payoutHandlers = payoutHandlers;
|
||||
_storeRepository = storeRepository;
|
||||
_networkProvider = networkProvider;
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
@ -102,6 +105,13 @@ namespace BTCPayServer.Controllers
|
||||
}).ToList()
|
||||
};
|
||||
vm.IsPending &= vm.AmountDue > 0.0m;
|
||||
|
||||
if (_pullPaymentHostedService.SupportsLNURL(blob))
|
||||
{
|
||||
var url = Url.Action("GetLNURLForPullPayment", "UILNURL", new { cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, pullPaymentId = vm.Id }, Request.Scheme, Request.Host.ToString());
|
||||
vm.LnurlEndpoint = url != null ? new Uri(url) : null;
|
||||
}
|
||||
|
||||
return View(nameof(ViewPullPayment), vm);
|
||||
}
|
||||
|
||||
|
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();
|
||||
}
|
@ -29,6 +29,7 @@ using BTCPayServer.Storage.Services;
|
||||
using BTCPayServer.Storage.Services.Providers;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
@ -664,6 +665,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[Route("lnd-config/{configKey}/lnd.config")]
|
||||
[AllowAnonymous]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public IActionResult GetLNDConfig(ulong configKey)
|
||||
{
|
||||
var conf = _LnConfigProvider.GetConfig(configKey);
|
||||
|
@ -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);
|
||||
@ -938,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";
|
||||
@ -1004,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
|
||||
|
@ -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;
|
||||
@ -216,7 +212,9 @@ namespace BTCPayServer.Controllers
|
||||
WalletId walletId,
|
||||
string? labelFilter = null,
|
||||
int skip = 0,
|
||||
int count = 50
|
||||
int count = 50,
|
||||
bool loadTransactions = false,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var paymentMethod = GetDerivationSchemeSettings(walletId);
|
||||
@ -227,25 +225,25 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
// We can't filter at the database level if we need to apply label filter
|
||||
var preFiltering = string.IsNullOrEmpty(labelFilter);
|
||||
var transactions = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, preFiltering ? skip : null, preFiltering ? count : null);
|
||||
var walletTransactionsInfo = await WalletRepository.GetWalletTransactionsInfo(walletId, transactions.Select(t => t.TransactionId.ToString()).ToArray());
|
||||
var model = new ListTransactionsViewModel { Skip = skip, Count = count };
|
||||
model.Labels.AddRange(
|
||||
(await WalletRepository.GetWalletLabels(walletId))
|
||||
.Select(c => (c.Label, c.Color, ColorPalette.Default.TextColor(c.Color))));
|
||||
|
||||
IList<TransactionHistoryLine>? transactions = null;
|
||||
Dictionary<string, WalletTransactionInfo>? walletTransactionsInfo = null;
|
||||
if (loadTransactions)
|
||||
{
|
||||
transactions = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, preFiltering ? skip : null, preFiltering ? count : null, cancellationToken: cancellationToken);
|
||||
walletTransactionsInfo = await WalletRepository.GetWalletTransactionsInfo(walletId, transactions.Select(t => t.TransactionId.ToString()).ToArray());
|
||||
}
|
||||
|
||||
if (labelFilter != null)
|
||||
{
|
||||
model.PaginationQuery = new Dictionary<string, object> { { "labelFilter", labelFilter } };
|
||||
}
|
||||
if (transactions == null)
|
||||
if (transactions == null || walletTransactionsInfo is null)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message =
|
||||
"There was an error retrieving the transactions list. Is NBXplorer configured correctly?"
|
||||
});
|
||||
model.Transactions = new List<ListTransactionsViewModel.TransactionViewModel>();
|
||||
}
|
||||
else
|
||||
@ -1315,7 +1313,7 @@ namespace BTCPayServer.Controllers
|
||||
[HttpGet("{walletId}/export")]
|
||||
public async Task<IActionResult> Export(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
|
||||
string format, string? labelFilter = null)
|
||||
string format, string? labelFilter = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var paymentMethod = GetDerivationSchemeSettings(walletId);
|
||||
if (paymentMethod == null)
|
||||
@ -1323,7 +1321,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
|
||||
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId, (string[]?)null);
|
||||
var input = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation);
|
||||
var input = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, cancellationToken: cancellationToken);
|
||||
var walletTransactionsInfo = await walletTransactionsInfoAsync;
|
||||
var export = new TransactionsExport(wallet, walletTransactionsInfo);
|
||||
var res = export.Process(input, format);
|
||||
|
@ -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();
|
||||
|
@ -199,7 +199,9 @@ namespace BTCPayServer.Data
|
||||
{ "GTQ", "bitpay" },
|
||||
{ "COP", "yadio" },
|
||||
{ "JPY", "bitbank" },
|
||||
{ "TRY", "btcturk" }
|
||||
{ "TRY", "btcturk" },
|
||||
{ "UGX", "exchangeratehost"},
|
||||
{ "RSD", "bitpay"}
|
||||
};
|
||||
|
||||
public string GetRecommendedExchange() =>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
@ -103,16 +105,31 @@ namespace BTCPayServer
|
||||
|
||||
public static decimal RoundUp(decimal value, int precision)
|
||||
{
|
||||
for (int i = 0; i < precision; i++)
|
||||
try
|
||||
{
|
||||
value = value * 10m;
|
||||
for (int i = 0; i < precision; i++)
|
||||
{
|
||||
value = value * 10m;
|
||||
}
|
||||
value = Math.Ceiling(value);
|
||||
for (int i = 0; i < precision; i++)
|
||||
{
|
||||
value = value / 10m;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
value = Math.Ceiling(value);
|
||||
for (int i = 0; i < precision; i++)
|
||||
catch (OverflowException)
|
||||
{
|
||||
value = value / 10m;
|
||||
return value;
|
||||
}
|
||||
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)
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -46,6 +46,8 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
public class PullPaymentHostedService : BaseAsyncService
|
||||
{
|
||||
private readonly string[] _lnurlSupportedCurrencies = { "BTC", "SATS" };
|
||||
|
||||
public class CancelRequest
|
||||
{
|
||||
public CancelRequest(string pullPaymentId)
|
||||
@ -337,6 +339,14 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
public bool SupportsLNURL(PullPaymentBlob blob)
|
||||
{
|
||||
var pms = blob.SupportedPaymentMethods.FirstOrDefault(id =>
|
||||
id.PaymentType == LightningPaymentType.Instance &&
|
||||
_networkProvider.DefaultNetwork.CryptoCode == id.CryptoCode);
|
||||
return pms is not null && _lnurlSupportedCurrencies.Contains(blob.Currency);
|
||||
}
|
||||
|
||||
public Task<RateResult> GetRate(PayoutData payout, string explicitRateRule, CancellationToken cancellationToken)
|
||||
{
|
||||
var ppBlob = payout.PullPaymentData?.GetBlob();
|
||||
|
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;
|
||||
}
|
||||
}
|
@ -105,6 +105,8 @@ namespace BTCPayServer.HostedServices
|
||||
var newDeliveryBlob = new WebhookDeliveryBlob();
|
||||
newDeliveryBlob.Request = oldDeliveryBlob.Request;
|
||||
var webhookEvent = newDeliveryBlob.ReadRequestAs<WebhookEvent>();
|
||||
if (webhookEvent.IsPruned())
|
||||
return null;
|
||||
webhookEvent.DeliveryId = newDelivery.Id;
|
||||
webhookEvent.WebhookId = webhookDelivery.Webhook.Id;
|
||||
// if we redelivered a redelivery, we still want the initial delivery here
|
||||
|
@ -350,6 +350,9 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<HostedServices.WebhookSender>();
|
||||
services.AddSingleton<IHostedService, WebhookSender>(o => o.GetRequiredService<WebhookSender>());
|
||||
services.AddSingleton<IHostedService, StoreEmailRuleProcessorSender>();
|
||||
services.AddSingleton<IHostedService, PeriodicTaskLauncherHostedService>();
|
||||
services.AddScheduledTask<CleanupWebhookDeliveriesTask>(TimeSpan.FromHours(6.0));
|
||||
|
||||
services.AddHttpClient(WebhookSender.OnionNamedClient)
|
||||
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
||||
services.AddHttpClient(WebhookSender.LoopbackNamedClient)
|
||||
@ -517,6 +520,8 @@ namespace BTCPayServer.Hosting
|
||||
services.AddRateProvider<BitflyerRateProvider>();
|
||||
services.AddRateProvider<YadioRateProvider>();
|
||||
services.AddRateProvider<BtcTurkRateProvider>();
|
||||
services.AddRateProvider<FreeCurrencyRatesRateProvider>();
|
||||
services.AddRateProvider<ExchangeRateHostRateProvider>();
|
||||
|
||||
// Broken
|
||||
// Providers.Add("argoneum", new ArgoneumRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_ARGONEUM")));
|
||||
|
@ -1,16 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Fido2;
|
||||
using BTCPayServer.Fido2.Models;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Plugins.Crowdfund;
|
||||
@ -26,17 +27,19 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBXplorer;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PeterO.Cbor;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using YamlDotNet.Serialization;
|
||||
using LightningAddressData = BTCPayServer.Data.LightningAddressData;
|
||||
using Serializer = NBXplorer.Serializer;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
public class MigrationStartupTask : IStartupTask
|
||||
{
|
||||
public Logs Logs { get; }
|
||||
|
||||
private readonly ApplicationDbContextFactory _DBContextFactory;
|
||||
private readonly StoreRepository _StoreRepository;
|
||||
@ -108,12 +111,6 @@ namespace BTCPayServer.Hosting
|
||||
settings.DeprecatedLightningConnectionStringCheck = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
if (!settings.UnreachableStoreCheck)
|
||||
{
|
||||
await UnreachableStoreCheck();
|
||||
settings.UnreachableStoreCheck = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
if (!settings.ConvertMultiplierToSpread)
|
||||
{
|
||||
await ConvertMultiplierToSpread();
|
||||
@ -246,6 +243,11 @@ namespace BTCPayServer.Hosting
|
||||
{
|
||||
await FixMappedDomainAppType();
|
||||
settings.FixMappedDomainAppType = true;
|
||||
}
|
||||
if (!settings.MigrateAppYmlToJson)
|
||||
{
|
||||
await MigrateAppYmlToJson();
|
||||
settings.MigrateAppYmlToJson = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
}
|
||||
@ -304,6 +306,134 @@ namespace BTCPayServer.Hosting
|
||||
return;
|
||||
await ToPostgresMigrationStartupTask.UpdateSequenceInvoiceSearch(ctx);
|
||||
}
|
||||
private async Task MigrateAppYmlToJson()
|
||||
{
|
||||
await using var ctx = _DBContextFactory.CreateContext();
|
||||
var apps = await ctx.Apps.Where(data => CrowdfundAppType.AppType == data.AppType || PointOfSaleAppType.AppType == data.AppType)
|
||||
.ToListAsync();
|
||||
foreach (var app in apps)
|
||||
{
|
||||
switch (app.AppType)
|
||||
{
|
||||
case CrowdfundAppType.AppType :
|
||||
var cfSettings = app.GetSettings<CrowdfundSettings>();
|
||||
if (!string.IsNullOrEmpty(cfSettings?.PerksTemplate))
|
||||
{
|
||||
cfSettings.PerksTemplate = AppService.SerializeTemplate(ParsePOSYML(cfSettings?.PerksTemplate));
|
||||
app.SetSettings(cfSettings);
|
||||
}
|
||||
break;
|
||||
case PointOfSaleAppType.AppType:
|
||||
var pSettings = app.GetSettings<PointOfSaleSettings>();
|
||||
if (!string.IsNullOrEmpty(pSettings?.Template))
|
||||
{
|
||||
pSettings.Template = AppService.SerializeTemplate(ParsePOSYML(pSettings?.Template));
|
||||
app.SetSettings(pSettings);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
}
|
||||
public static ViewPointOfSaleViewModel.Item[] ParsePOSYML(string yaml)
|
||||
{
|
||||
var items = new List<ViewPointOfSaleViewModel.Item>();
|
||||
var stream = new YamlStream();
|
||||
stream.Load(new StringReader(yaml));
|
||||
|
||||
var root = stream.Documents.First().RootNode as YamlMappingNode;
|
||||
foreach (var posItem in root.Children)
|
||||
{
|
||||
var trimmedKey = ((YamlScalarNode)posItem.Key).Value?.Trim();
|
||||
if (string.IsNullOrEmpty(trimmedKey))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var currentItem = new ViewPointOfSaleViewModel.Item
|
||||
{
|
||||
Id = trimmedKey, Title = trimmedKey, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed
|
||||
};
|
||||
var itemSpecs = (YamlMappingNode)posItem.Value;
|
||||
foreach (var spec in itemSpecs)
|
||||
{
|
||||
if (spec.Key is not YamlScalarNode {Value: string keyString} || string.IsNullOrEmpty(keyString))
|
||||
continue;
|
||||
var scalarValue = spec.Value as YamlScalarNode;
|
||||
switch (keyString)
|
||||
{
|
||||
case "title":
|
||||
currentItem.Title = scalarValue?.Value ?? trimmedKey;
|
||||
break;
|
||||
case "inventory":
|
||||
if (int.TryParse(scalarValue?.Value, out var inv))
|
||||
{
|
||||
currentItem.Inventory = inv;
|
||||
}
|
||||
break;
|
||||
case "description":
|
||||
currentItem.Description = scalarValue?.Value;
|
||||
break;
|
||||
case "image":
|
||||
currentItem.Image = scalarValue?.Value;
|
||||
break;
|
||||
case "payment_methods" when spec.Value is YamlSequenceNode pmSequenceNode:
|
||||
|
||||
currentItem.PaymentMethods = pmSequenceNode.Children
|
||||
.Select(node => (node as YamlScalarNode)?.Value?.Trim())
|
||||
.Where(node => !string.IsNullOrEmpty(node)).ToArray();
|
||||
break;
|
||||
case "price_type":
|
||||
case "custom":
|
||||
if (bool.TryParse(scalarValue?.Value, out var customBoolValue))
|
||||
{
|
||||
if (customBoolValue)
|
||||
{
|
||||
currentItem.PriceType = currentItem.Price is null or 0
|
||||
? ViewPointOfSaleViewModel.ItemPriceType.Topup
|
||||
: ViewPointOfSaleViewModel.ItemPriceType.Minimum;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentItem.PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed;
|
||||
}
|
||||
}
|
||||
else if (Enum.TryParse<ViewPointOfSaleViewModel.ItemPriceType>(scalarValue?.Value, true,
|
||||
out var customPriceType))
|
||||
{
|
||||
currentItem.PriceType = customPriceType;
|
||||
}
|
||||
|
||||
break;
|
||||
case "price":
|
||||
if (decimal.TryParse(scalarValue?.Value, out var price))
|
||||
{
|
||||
currentItem.Price = price;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "buybuttontext":
|
||||
currentItem.BuyButtonText = scalarValue?.Value;
|
||||
break;
|
||||
|
||||
case "disabled":
|
||||
if (bool.TryParse(scalarValue?.Value, out var disabled))
|
||||
{
|
||||
currentItem.Disabled = disabled;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
items.Add(currentItem);
|
||||
}
|
||||
|
||||
return items.ToArray();
|
||||
}
|
||||
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
|
||||
@ -510,8 +640,6 @@ WHERE cte.""Id""=p.""Id""
|
||||
await using var ctx = _DBContextFactory.CreateContext();
|
||||
foreach (var app in await ctx.Apps.Include(data => data.StoreData).AsQueryable().ToArrayAsync())
|
||||
{
|
||||
ViewPointOfSaleViewModel.Item[] items;
|
||||
string newTemplate;
|
||||
switch (app.AppType)
|
||||
{
|
||||
case CrowdfundAppType.AppType:
|
||||
@ -521,13 +649,6 @@ WHERE cte.""Id""=p.""Id""
|
||||
settings1.TargetCurrency = app.StoreData.GetStoreBlob().DefaultCurrency;
|
||||
app.SetSettings(settings1);
|
||||
}
|
||||
items = _appService.Parse(settings1.PerksTemplate, settings1.TargetCurrency);
|
||||
newTemplate = _appService.SerializeTemplate(items);
|
||||
if (settings1.PerksTemplate != newTemplate)
|
||||
{
|
||||
settings1.PerksTemplate = newTemplate;
|
||||
app.SetSettings(settings1);
|
||||
};
|
||||
break;
|
||||
|
||||
case PointOfSaleAppType.AppType:
|
||||
@ -538,13 +659,6 @@ WHERE cte.""Id""=p.""Id""
|
||||
settings2.Currency = app.StoreData.GetStoreBlob().DefaultCurrency;
|
||||
app.SetSettings(settings2);
|
||||
}
|
||||
items = _appService.Parse(settings2.Template, settings2.Currency);
|
||||
newTemplate = _appService.SerializeTemplate(items);
|
||||
if (settings2.Template != newTemplate)
|
||||
{
|
||||
settings2.Template = newTemplate;
|
||||
app.SetSettings(settings2);
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -932,11 +1046,6 @@ retry:
|
||||
}
|
||||
}
|
||||
|
||||
private Task UnreachableStoreCheck()
|
||||
{
|
||||
return _StoreRepository.CleanUnreachableStores();
|
||||
}
|
||||
|
||||
private async Task DeprecatedLightningConnectionStringCheck()
|
||||
{
|
||||
using var ctx = _DBContextFactory.CreateContext();
|
||||
|
@ -213,6 +213,9 @@ namespace BTCPayServer.Hosting
|
||||
{
|
||||
var typeMapping = t.EntityTypeMappings.Single();
|
||||
var query = (IQueryable<object>)otherContext.GetType().GetMethod("Set", new Type[0])!.MakeGenericMethod(typeMapping.EntityType.ClrType).Invoke(otherContext, null)!;
|
||||
if (t.Name == "WebhookDeliveries" ||
|
||||
t.Name == "InvoiceWebhookDeliveries")
|
||||
continue;
|
||||
Logger.LogInformation($"Migrating table: " + t.Name);
|
||||
List<PropertyInfo> datetimeProperties = new List<PropertyInfo>();
|
||||
foreach (var col in t.Columns)
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Stores;
|
||||
|
||||
|
||||
namespace BTCPayServer.Models.AppViewModels
|
||||
@ -14,12 +15,12 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
public string AppName { get; set; }
|
||||
public string AppType { get; set; }
|
||||
public string ViewStyle { get; set; }
|
||||
public bool IsOwner { get; set; }
|
||||
|
||||
public string UpdateAction { get { return "Update" + AppType; } }
|
||||
public string ViewAction { get { return "View" + AppType; } }
|
||||
public DateTimeOffset Created { get; set; }
|
||||
public AppData App { get; set; }
|
||||
public StoreRepository.StoreRole Role { get; set; }
|
||||
}
|
||||
|
||||
public ListAppViewModel[] Apps { get; set; }
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user