Compare commits
50 Commits
parallel-p
...
v1.13.1
Author | SHA1 | Date | |
---|---|---|---|
ca55e1f300 | |||
8b02c0bd82 | |||
b92ff7c27b | |||
d24761a498 | |||
c78ee24d0a | |||
172dd507bd | |||
fdd4790023 | |||
4ebe46830b | |||
a2df9ed44c | |||
6ae474d214 | |||
5b31d4de20 | |||
14f8c73b08 | |||
529075f64c | |||
dba102e74f | |||
0f3f8b6bf9 | |||
83028b9b73 | |||
1fe766cb16 | |||
6b45eb0d3d | |||
6b0087ab69 | |||
e60fd8d6ab | |||
88a1d83323 | |||
e21a8df0f3 | |||
93f37b506b | |||
fca3480e37 | |||
966547db54 | |||
09dbe44bca | |||
b7ce6b7400 | |||
78f169cd24 | |||
d0e11f1ec4 | |||
912a706de9 | |||
9b5c8a8254 | |||
e5adc630af | |||
c56c6401d6 | |||
0e64df3bbf | |||
e497903bf4 | |||
f1ff913cbe | |||
3a00d32ce0 | |||
f0f698f411 | |||
22c6468a5d | |||
a60072a431 | |||
dcc6f17c9c | |||
15ce148b99 | |||
3b73d5a5cb | |||
1fd3054006 | |||
2db1434929 | |||
a171671fe5 | |||
9160a1d71e | |||
a896560a3c | |||
e43b4ed540 | |||
8b446e2791 |
.vscode
BTCPayServer.Abstractions
BTCPayServer.Client
BTCPayServer.Data
BTCPayServer.Rating/Providers
BTCPayServer.Tests
AltcoinTests
GreenfieldAPITests.csSeleniumTester.csSeleniumTests.csTestAccount.csTestData
ThirdPartyTests.csUnitTest1.csdocker-compose.altcoins.ymldocker-compose.ymlBTCPayServer
BTCPayServer.csprojStorePolicies.csUserManagerExtensions.cs
Components/MainNav
Controllers
GreenField
GreenfieldAppsController.csGreenfieldCustodianAccountController.csGreenfieldPullPaymentController.csGreenfieldStoreRolesController.csGreenfieldStoreUsersController.csGreenfieldUsersController.cs
UIAccountController.csUIHomeController.csUIInvoiceController.UI.csUIManageController.2FA.csUIManageController.Notifications.csUIPaymentRequestController.csUIPullPaymentController.csUIServerController.Roles.csUIServerController.Users.csUIServerController.csUIStorePullPaymentsController.PullPayments.csUIStoresController.Dashboard.csUIStoresController.Email.csUIStoresController.Integrations.csUIStoresController.LightningLike.csUIStoresController.Onchain.csUIStoresController.Roles.csUIStoresController.Users.csUIStoresController.csUIUserStoresController.csUIWalletsController.csData
Payouts
PullPayments
Events
Extensions
Forms
HostedServices
Hosting
Models
Payments/Lightning
PayoutProcessors
Plugins
Altcoins/Monero/RPC/Models
Crowdfund
PointOfSale
Security
Services
Altcoins/Monero
MoneroLikeExtensions.cs
Payments
MoneroLikeOnChainPaymentMethodDetails.csMoneroLikePaymentData.csMoneroLikePaymentMethodHandler.csMoneroSupportedPaymentMethod.cs
Services
UI
Apps
Invoices
Mails
Notifications
ReportService.csReporting
Stores
UserService.csViews
Shared
CameraScanner.cshtml
Crowdfund
EmailsBody.cshtmlEmailsTest.cshtmlListRoles.cshtmlPointOfSale
TemplateEditor.cshtml_BTCPaySupporters.cshtml_Confirm.cshtml_LayoutSignedOut.cshtmlUIAccount
UIForms
UIInvoice
UILightningAutomatedPayoutProcessors
UIManage
UIMoneroLikeStore
UIOnChainAutomatedPayoutProcessors
UIPaymentRequest
UIPayoutProcessors
UIPullPayment
UIServer
UIStorePullPayments
UIStores
CheckoutAppearance.cshtmlDashboard.cshtmlGeneralSettings.cshtml
ImportWallet
Index.cshtmlLightningSettings.cshtmlListTokens.cshtmlRates.cshtmlSetupLightningNode.cshtmlStoreEmailSettings.cshtmlStoreEmails.cshtmlStoreUsers.cshtmlWalletSettings.cshtmlWebhooks.cshtml_GenerateWalletForm.cshtml_Nav.cshtmlUIUserStores
UIWallets
wwwroot
Build
Changelog.mdREADME.md
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -10,7 +10,7 @@
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/BTCPayServer/bin/Debug/net6.0/BTCPayServer.dll",
|
||||
"program": "${workspaceFolder}/BTCPayServer/bin/Debug/net8.0/BTCPayServer.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/BTCPayServer",
|
||||
"stopAtEntry": false,
|
||||
|
@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Npgsql;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations;
|
||||
|
||||
@ -85,10 +86,9 @@ namespace BTCPayServer.Abstractions.Contracts
|
||||
{
|
||||
o.EnableRetryOnFailure(10);
|
||||
o.SetPostgresVersion(12, 0);
|
||||
if (!string.IsNullOrEmpty(_schemaPrefix))
|
||||
{
|
||||
o.MigrationsHistoryTable(_schemaPrefix);
|
||||
}
|
||||
var mainSearchPath = GetSearchPath(_options.Value.ConnectionString);
|
||||
var schemaPrefix = string.IsNullOrEmpty(_schemaPrefix) ? "__EFMigrationsHistory" : _schemaPrefix;
|
||||
o.MigrationsHistoryTable(schemaPrefix, mainSearchPath);
|
||||
})
|
||||
.ReplaceService<IMigrationsSqlGenerator, CustomNpgsqlMigrationsSqlGenerator>();
|
||||
break;
|
||||
@ -108,5 +108,11 @@ namespace BTCPayServer.Abstractions.Contracts
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSearchPath(string connectionString)
|
||||
{
|
||||
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString);
|
||||
var searchPaths = connectionStringBuilder.SearchPath?.Split(',');
|
||||
return searchPaths is not { Length: > 0 } ? null : searchPaths[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
|
||||
namespace BTCPayServer.Abstractions.TagHelpers;
|
||||
|
||||
[HtmlTargetElement("form", Attributes = "[permissioned]")]
|
||||
public partial class PermissionedFormTagHelper(
|
||||
IAuthorizationService authorizationService,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: TagHelper
|
||||
{
|
||||
public string Permissioned { get; set; }
|
||||
public string PermissionResource { get; set; }
|
||||
|
||||
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
if (httpContextAccessor.HttpContext is null || string.IsNullOrEmpty(Permissioned))
|
||||
return;
|
||||
|
||||
var res = await authorizationService.AuthorizeAsync(httpContextAccessor.HttpContext.User,
|
||||
PermissionResource, Permissioned);
|
||||
if (!res.Succeeded)
|
||||
{
|
||||
var content = await output.GetChildContentAsync();
|
||||
var html = SubmitButtonRegex().Replace(content.GetContent(), "");
|
||||
output.Content.SetHtmlContent($"<fieldset disabled>{html}</fieldset>");
|
||||
}
|
||||
}
|
||||
|
||||
[GeneratedRegex("<(button|input).*?type=\"submit\".*?>.*?</\\1>")]
|
||||
private static partial Regex SubmitButtonRegex();
|
||||
}
|
@ -16,7 +16,7 @@
|
||||
<Platforms>AnyCPU</Platforms>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<Version Condition=" '$(Version)' == '' ">1.7.3</Version>
|
||||
<Version Condition=" '$(Version)' == '' ">1.7.4</Version>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
|
@ -26,6 +26,7 @@ namespace BTCPayServer.Client.Models
|
||||
public string Template { get; set; } = null;
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public PosViewType DefaultView { get; set; }
|
||||
public bool ShowItems { get; set; } = false;
|
||||
public bool ShowCustomAmount { get; set; } = false;
|
||||
public bool ShowDiscount { get; set; } = false;
|
||||
public bool ShowSearch { get; set; } = true;
|
||||
|
@ -1,3 +1,5 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class EmailSettingsData
|
||||
@ -26,4 +28,11 @@ public class EmailSettingsData
|
||||
get; set;
|
||||
}
|
||||
public bool DisableCertificateCheck { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool EnabledCertificateCheck
|
||||
{
|
||||
get => !DisableCertificateCheck;
|
||||
set { DisableCertificateCheck = !value; }
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string DefaultView { get; set; }
|
||||
public bool ShowItems { get; set; }
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
public bool ShowDiscount { get; set; }
|
||||
public bool ShowSearch { get; set; }
|
||||
|
@ -4,6 +4,7 @@ using System.Text;
|
||||
using NBitcoin.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
@ -14,11 +15,15 @@ namespace BTCPayServer.Client.Models
|
||||
}
|
||||
public class RegisterBoltcardRequest
|
||||
{
|
||||
[JsonProperty("LNURLW")]
|
||||
public string LNURLW { get; set; }
|
||||
[JsonConverter(typeof(HexJsonConverter))]
|
||||
[JsonProperty("UID")]
|
||||
public byte[] UID { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public OnExistingBehavior? OnExisting { get; set; }
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; } = new Dictionary<string, JToken>();
|
||||
}
|
||||
public class RegisterBoltcardResponse
|
||||
{
|
||||
|
@ -107,10 +107,10 @@ namespace BTCPayServer.Data
|
||||
//PayjoinLock.OnModelCreating(builder);
|
||||
PaymentRequestData.OnModelCreating(builder, Database);
|
||||
PaymentData.OnModelCreating(builder, Database);
|
||||
PayoutData.OnModelCreating(builder);
|
||||
PayoutData.OnModelCreating(builder, Database);
|
||||
PendingInvoiceData.OnModelCreating(builder);
|
||||
//PlannedTransaction.OnModelCreating(builder);
|
||||
PullPaymentData.OnModelCreating(builder);
|
||||
PullPaymentData.OnModelCreating(builder, Database);
|
||||
RefundData.OnModelCreating(builder);
|
||||
SettingData.OnModelCreating(builder, Database);
|
||||
StoreSettingData.OnModelCreating(builder, Database);
|
||||
|
@ -1,8 +1,11 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
@ -21,14 +24,14 @@ namespace BTCPayServer.Data
|
||||
[MaxLength(20)]
|
||||
[Required]
|
||||
public string PaymentMethodId { get; set; }
|
||||
public byte[] Blob { get; set; }
|
||||
public byte[] Proof { get; set; }
|
||||
public string Blob { get; set; }
|
||||
public string Proof { get; set; }
|
||||
#nullable enable
|
||||
public string? Destination { get; set; }
|
||||
#nullable restore
|
||||
public StoreData StoreData { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder)
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
builder.Entity<PayoutData>()
|
||||
.HasOne(o => o.PullPaymentData)
|
||||
@ -43,6 +46,33 @@ namespace BTCPayServer.Data
|
||||
.HasIndex(o => o.State);
|
||||
builder.Entity<PayoutData>()
|
||||
.HasIndex(x => new { DestinationId = x.Destination, x.State });
|
||||
|
||||
if (databaseFacade.IsNpgsql())
|
||||
{
|
||||
builder.Entity<PayoutData>()
|
||||
.Property(o => o.Blob)
|
||||
.HasColumnType("JSONB");
|
||||
builder.Entity<PayoutData>()
|
||||
.Property(o => o.Proof)
|
||||
.HasColumnType("JSONB");
|
||||
}
|
||||
else if (databaseFacade.IsMySql())
|
||||
{
|
||||
builder.Entity<PayoutData>()
|
||||
.Property(o => o.Blob)
|
||||
.HasConversion(new ValueConverter<string, byte[]>
|
||||
(
|
||||
convertToProviderExpression: (str) => Encoding.UTF8.GetBytes(str),
|
||||
convertFromProviderExpression: (bytes) => Encoding.UTF8.GetString(bytes)
|
||||
));
|
||||
builder.Entity<PayoutData>()
|
||||
.Property(o => o.Proof)
|
||||
.HasConversion(new ValueConverter<string, byte[]>
|
||||
(
|
||||
convertToProviderExpression: (str) => Encoding.UTF8.GetBytes(str),
|
||||
convertFromProviderExpression: (bytes) => Encoding.UTF8.GetString(bytes)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// utility methods
|
||||
|
@ -3,8 +3,11 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
@ -24,16 +27,33 @@ namespace BTCPayServer.Data
|
||||
public DateTimeOffset? EndDate { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
public List<PayoutData> Payouts { get; set; }
|
||||
public byte[] Blob { get; set; }
|
||||
public string Blob { get; set; }
|
||||
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder)
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
builder.Entity<PullPaymentData>()
|
||||
.HasIndex(o => o.StoreId);
|
||||
builder.Entity<PullPaymentData>()
|
||||
.HasOne(o => o.StoreData)
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(o => o.PullPayments).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
if (databaseFacade.IsNpgsql())
|
||||
{
|
||||
builder.Entity<PullPaymentData>()
|
||||
.Property(o => o.Blob)
|
||||
.HasColumnType("JSONB");
|
||||
}
|
||||
else if (databaseFacade.IsMySql())
|
||||
{
|
||||
builder.Entity<PullPaymentData>()
|
||||
.Property(o => o.Blob)
|
||||
.HasConversion(new ValueConverter<string, byte[]>
|
||||
(
|
||||
convertToProviderExpression: (str) => Encoding.UTF8.GetBytes(str),
|
||||
convertFromProviderExpression: (bytes) => Encoding.UTF8.GetString(bytes)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public (DateTimeOffset Start, DateTimeOffset? End)? GetPeriod(DateTimeOffset now)
|
||||
|
@ -0,0 +1,28 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240229000000_PayoutAndPullPaymentToJsonBlob")]
|
||||
public partial class PayoutAndPullPaymentToJsonBlob : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (migrationBuilder.IsNpgsql())
|
||||
{
|
||||
migrationBuilder.Sql("ALTER TABLE \"Payouts\" ALTER COLUMN \"Blob\" TYPE JSONB USING regexp_replace(convert_from(\"Blob\",'UTF8'), '\\\\u0000', '', 'g')::JSONB");
|
||||
migrationBuilder.Sql("ALTER TABLE \"Payouts\" ALTER COLUMN \"Proof\" TYPE JSONB USING regexp_replace(convert_from(\"Proof\",'UTF8'), '\\\\u0000', '', 'g')::JSONB");
|
||||
migrationBuilder.Sql("ALTER TABLE \"PullPayments\" ALTER COLUMN \"Blob\" TYPE JSONB USING regexp_replace(convert_from(\"Blob\",'UTF8'), '\\\\u0000', '', 'g')::JSONB");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240229092905_AddManagerAndEmployeeToStoreRoles")]
|
||||
public partial class AddManagerAndEmployeeToStoreRoles : Migration
|
||||
{
|
||||
object GetPermissionsData(MigrationBuilder migrationBuilder, string[] permissions)
|
||||
{
|
||||
return migrationBuilder.IsNpgsql()
|
||||
? permissions
|
||||
: JsonConvert.SerializeObject(permissions);
|
||||
}
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
var permissionsType = migrationBuilder.IsNpgsql() ? "TEXT[]" : "TEXT";
|
||||
migrationBuilder.InsertData(
|
||||
"StoreRoles",
|
||||
columns: new[] { "Id", "Role", "Permissions" },
|
||||
columnTypes: new[] { "TEXT", "TEXT", permissionsType },
|
||||
values: new object[,]
|
||||
{
|
||||
{
|
||||
"Manager", "Manager", GetPermissionsData(migrationBuilder, new[]
|
||||
{
|
||||
"btcpay.store.canviewstoresettings",
|
||||
"btcpay.store.canmodifyinvoices",
|
||||
"btcpay.store.webhooks.canmodifywebhooks",
|
||||
"btcpay.store.canmodifypaymentrequests",
|
||||
"btcpay.store.canmanagepullpayments",
|
||||
"btcpay.store.canmanagepayouts"
|
||||
})
|
||||
},
|
||||
{
|
||||
"Employee", "Employee", GetPermissionsData(migrationBuilder, new[]
|
||||
{
|
||||
"btcpay.store.canmodifyinvoices",
|
||||
"btcpay.store.canmodifypaymentrequests",
|
||||
"btcpay.store.cancreatenonapprovedpullpayments",
|
||||
"btcpay.store.canviewpayouts",
|
||||
"btcpay.store.canviewpullpayments"
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
"StoreRoles",
|
||||
keyColumns: new[] { "Id" },
|
||||
keyColumnTypes: new[] { "TEXT" },
|
||||
keyValues: new[] { "Guest" },
|
||||
columns: new[] { "Permissions" },
|
||||
columnTypes: new[] { permissionsType },
|
||||
values: new object[]
|
||||
{
|
||||
GetPermissionsData(migrationBuilder, new[]
|
||||
{
|
||||
"btcpay.store.canmodifyinvoices",
|
||||
"btcpay.store.canviewpaymentrequests",
|
||||
"btcpay.store.canviewpullpayments",
|
||||
"btcpay.store.canviewpayouts"
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DeleteData("StoreRoles", "Id", "Manager");
|
||||
migrationBuilder.DeleteData("StoreRoles", "Id", "Employee");
|
||||
|
||||
var permissionsType = migrationBuilder.IsNpgsql() ? "TEXT[]" : "TEXT";
|
||||
migrationBuilder.UpdateData(
|
||||
"StoreRoles",
|
||||
keyColumns: new[] { "Id" },
|
||||
keyColumnTypes: new[] { "TEXT" },
|
||||
keyValues: new[] { "Guest" },
|
||||
columns: new[] { "Permissions" },
|
||||
columnTypes: new[] { permissionsType },
|
||||
values: new object[]
|
||||
{
|
||||
GetPermissionsData(migrationBuilder, new[]
|
||||
{
|
||||
"btcpay.store.canviewstoresettings",
|
||||
"btcpay.store.canmodifyinvoices",
|
||||
"btcpay.store.canviewcustodianaccounts",
|
||||
"btcpay.store.candeposittocustodianaccount"
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
// <auto-generated />
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -16,7 +16,7 @@ namespace BTCPayServer.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.1");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
@ -599,7 +599,7 @@ namespace BTCPayServer.Migrations
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
.HasColumnType("BLOB");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
@ -613,7 +613,7 @@ namespace BTCPayServer.Migrations
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Proof")
|
||||
.HasColumnType("BLOB");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PullPaymentDataId")
|
||||
.HasColumnType("TEXT");
|
||||
@ -704,7 +704,7 @@ namespace BTCPayServer.Migrations
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
.HasColumnType("BLOB");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("EndDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
@ -9,7 +9,7 @@ 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");
|
||||
public RateSourceInfo RateSourceInfo => new("free-currency-rates", "Free Currency Rates", "https://currency-api.pages.dev/v1/currencies/btc.min.json");
|
||||
private readonly HttpClient _httpClient;
|
||||
public FreeCurrencyRatesRateProvider(HttpClient httpClient)
|
||||
{
|
||||
|
@ -125,7 +125,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<RedirectToActionResult>(response);
|
||||
|
||||
// Setting it again should show the confirmation page
|
||||
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, DerivationScheme = oldScheme });
|
||||
response = await controller.UpdateWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode, DerivationScheme = oldScheme });
|
||||
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
|
||||
Assert.True(setupVm.Confirmation);
|
||||
|
||||
@ -133,7 +133,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// cobo vault file
|
||||
var content = "{\"ExtPubKey\":\"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\"MasterFingerprint\":\"7a7563b5\",\"DerivationPath\":\"M\\/84'\\/0'\\/0'\",\"CoboVaultFirmwareVersion\":\"1.2.0(BTC-Only)\"}";
|
||||
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("cobovault.json", content)});
|
||||
response = await controller.UpdateWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("cobovault.json", content) });
|
||||
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
|
||||
Assert.True(setupVm.Confirmation);
|
||||
response = await controller.UpdateWallet(setupVm);
|
||||
@ -144,7 +144,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// wasabi wallet file
|
||||
content = "{\r\n \"EncryptedSecret\": \"6PYWBQ1zsukowsnTNA57UUx791aBuJusm7E4egXUmF5WGw3tcdG3cmTL57\",\r\n \"ChainCode\": \"waSIVbn8HaoovoQg/0t8IS1+ZCxGsJRGFT21i06nWnc=\",\r\n \"MasterFingerprint\": \"7a7563b5\",\r\n \"ExtPubKey\": \"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\r\n \"PasswordVerified\": false,\r\n \"MinGapLimit\": 21,\r\n \"AccountKeyPath\": \"84'/0'/0'\",\r\n \"BlockchainState\": {\r\n \"Network\": \"RegTest\",\r\n \"Height\": \"0\"\r\n },\r\n \"HdPubKeys\": []\r\n}";
|
||||
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("wasabi.json", content)});
|
||||
response = await controller.UpdateWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("wasabi.json", content) });
|
||||
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
|
||||
Assert.True(setupVm.Confirmation);
|
||||
response = await controller.UpdateWallet(setupVm);
|
||||
@ -155,13 +155,13 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Can we upload coldcard settings? (Should fail, we are giving a mainnet file to a testnet network)
|
||||
content = "{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
|
||||
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("coldcard-ypub.json", content)});
|
||||
response = await controller.UpdateWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("coldcard-ypub.json", content) });
|
||||
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
|
||||
Assert.False(setupVm.Confirmation); // Should fail, we are giving a mainnet file to a testnet network
|
||||
|
||||
// And with a good file? (upub)
|
||||
content = "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DBYp1qGgsTrkzCptMGZc2x18pquLwGrBw6nS59T4NViZ4cni1mGowQzziy85K8vzkp1jVtWrSkLhqk9KDfvrGeB369wGNYf39kX8rQfiLn\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
|
||||
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("coldcard-upub.json", content)});
|
||||
response = await controller.UpdateWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("coldcard-upub.json", content) });
|
||||
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
|
||||
Assert.True(setupVm.Confirmation);
|
||||
response = await controller.UpdateWallet(setupVm);
|
||||
@ -430,6 +430,7 @@ namespace BTCPayServer.Tests
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Altcoins", "Altcoins")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUsePaymentMethodDropdown()
|
||||
{
|
||||
using (var s = CreateSeleniumTester())
|
||||
@ -438,10 +439,10 @@ namespace BTCPayServer.Tests
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
s.GoToRegister();
|
||||
s.RegisterNewUser();
|
||||
s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
s.AddDerivationScheme("BTC");
|
||||
|
||||
s.EnableCheckout(Client.Models.CheckoutType.V1);
|
||||
//check that there is no dropdown since only one payment method is set
|
||||
var invoiceId = s.CreateInvoice(10, "USD", "a@g.com");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
@ -454,18 +455,25 @@ namespace BTCPayServer.Tests
|
||||
invoiceId = s.CreateInvoice(10, "USD", "a@g.com");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
var currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies"));
|
||||
Assert.Contains("BTC", currencyDropdownButton.Text);
|
||||
Assert.Contains("Bitcoin", currencyDropdownButton.Text);
|
||||
currencyDropdownButton.Click();
|
||||
|
||||
var elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem"));
|
||||
Assert.Equal(3, elements.Count);
|
||||
elements.Single(element => element.Text.Contains("LTC")).Click();
|
||||
IEnumerable<IWebElement> elements = null;
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem"));
|
||||
Assert.Equal(3, elements.Count());
|
||||
elements.Single(element => element.Text.Contains("Litecoin")).Click();
|
||||
});
|
||||
currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies"));
|
||||
Assert.Contains("LTC", currencyDropdownButton.Text);
|
||||
Assert.Contains("Litecoin", currencyDropdownButton.Text);
|
||||
currencyDropdownButton.Click();
|
||||
|
||||
elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem"));
|
||||
elements.Single(element => element.Text.Contains("Lightning")).Click();
|
||||
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem"));
|
||||
elements.Single(element => element.Text.Contains("Lightning")).Click();
|
||||
});
|
||||
|
||||
currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies"));
|
||||
Assert.Contains("Lightning", currencyDropdownButton.Text);
|
||||
@ -754,7 +762,7 @@ inventoryitem:
|
||||
inventory: 1
|
||||
noninventoryitem:
|
||||
price: 10.0";
|
||||
|
||||
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
|
||||
@ -866,7 +874,7 @@ g:
|
||||
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, choiceKey: "g").Result);
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
|
@ -13,6 +13,7 @@ using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
@ -24,6 +25,7 @@ using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -301,6 +303,16 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("test app title", app.Title);
|
||||
Assert.False(app.Archived);
|
||||
|
||||
// Test title falls back to name
|
||||
app = await client.CreatePointOfSaleApp(
|
||||
user.StoreId,
|
||||
new CreatePointOfSaleAppRequest
|
||||
{
|
||||
AppName = "test app name"
|
||||
}
|
||||
);
|
||||
Assert.Equal("test app name", app.Title);
|
||||
|
||||
// Make sure we return a 404 if we try to get an app that doesn't exist
|
||||
await AssertHttpError(404, async () =>
|
||||
{
|
||||
@ -470,6 +482,16 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("Crowdfund", app.AppType);
|
||||
Assert.False(app.Archived);
|
||||
|
||||
// Test title falls back to name
|
||||
app = await client.CreateCrowdfundApp(
|
||||
user.StoreId,
|
||||
new CreateCrowdfundAppRequest
|
||||
{
|
||||
AppName = "test app name"
|
||||
}
|
||||
);
|
||||
Assert.Equal("test app name", app.Title);
|
||||
|
||||
// Make sure we return a 404 if we try to get an app that doesn't exist
|
||||
await AssertHttpError(404, async () =>
|
||||
{
|
||||
@ -1115,6 +1137,35 @@ namespace BTCPayServer.Tests
|
||||
OnExisting = OnExistingBehavior.KeepVersion
|
||||
});
|
||||
Assert.Equal(card2.Version, card3.Version);
|
||||
var p = new byte[] { 0xc7 }.Concat(uid).Concat(new byte[8]).ToArray();
|
||||
var card4 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
|
||||
{
|
||||
OnExisting = OnExistingBehavior.KeepVersion,
|
||||
LNURLW = card2.LNURLW + $"?p={Encoders.Hex.EncodeData(AESKey.Parse(card2.K1).Encrypt(p))}"
|
||||
});
|
||||
Assert.Equal(card2.Version, card4.Version);
|
||||
Assert.Equal(card2.K4, card4.K4);
|
||||
// Can't define both properties
|
||||
await AssertValidationError(["LNURLW"], () => client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
|
||||
{
|
||||
OnExisting = OnExistingBehavior.KeepVersion,
|
||||
UID = uid,
|
||||
LNURLW = card2.LNURLW + $"?p={Encoders.Hex.EncodeData(AESKey.Parse(card2.K1).Encrypt(p))}"
|
||||
}));
|
||||
// p is malformed
|
||||
await AssertValidationError(["LNURLW"], () => client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
|
||||
{
|
||||
OnExisting = OnExistingBehavior.KeepVersion,
|
||||
UID = uid,
|
||||
LNURLW = card2.LNURLW + $"?p=lol"
|
||||
}));
|
||||
// p is invalid
|
||||
p[0] = 0;
|
||||
await AssertValidationError(["LNURLW"], () => client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
|
||||
{
|
||||
OnExisting = OnExistingBehavior.KeepVersion,
|
||||
LNURLW = card2.LNURLW + $"?p={Encoders.Hex.EncodeData(AESKey.Parse(card2.K1).Encrypt(p))}"
|
||||
}));
|
||||
// Test with SATS denomination values
|
||||
var testSats = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
|
||||
{
|
||||
@ -2287,7 +2338,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("updated", invoice.Metadata["itemCode"].Value<string>());
|
||||
Assert.Equal(15, ((JArray)invoice.Metadata["newstuff"]).Values<int>().Sum());
|
||||
|
||||
//also test the the metadata actually got saved
|
||||
//also test the metadata actually got saved
|
||||
invoice = await client.GetInvoice(user.StoreId, invoice.Id);
|
||||
Assert.Equal(newOrderId, invoice.Metadata["orderId"].Value<string>());
|
||||
Assert.Equal("updated", invoice.Metadata["itemCode"].Value<string>());
|
||||
@ -3439,7 +3490,6 @@ namespace BTCPayServer.Tests
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task StoreUsersAPITest()
|
||||
{
|
||||
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
|
||||
@ -3449,52 +3499,83 @@ namespace BTCPayServer.Tests
|
||||
var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings);
|
||||
|
||||
var roles = await client.GetServerRoles();
|
||||
Assert.Equal(2,roles.Count);
|
||||
Assert.Equal(4, roles.Count);
|
||||
#pragma warning disable CS0618
|
||||
var ownerRole = roles.Single(data => data.Role == StoreRoles.Owner);
|
||||
var managerRole = roles.Single(data => data.Role == StoreRoles.Manager);
|
||||
var employeeRole = roles.Single(data => data.Role == StoreRoles.Employee);
|
||||
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(ownerRole.Id, storeuser.Role);
|
||||
var user2 = tester.NewAccount();
|
||||
await user2.GrantAccessAsync(false);
|
||||
var storeUser = Assert.Single(users);
|
||||
Assert.Equal(user.UserId, storeUser.UserId);
|
||||
Assert.Equal(ownerRole.Id, storeUser.Role);
|
||||
var manager = tester.NewAccount();
|
||||
await manager.GrantAccessAsync();
|
||||
var employee = tester.NewAccount();
|
||||
await employee.GrantAccessAsync();
|
||||
var guest = tester.NewAccount();
|
||||
await guest.GrantAccessAsync();
|
||||
|
||||
var user2Client = await user2.CreateClient(Policies.CanModifyStoreSettings);
|
||||
var managerClient = await manager.CreateClient(Policies.CanModifyStoreSettings);
|
||||
var employeeClient = await employee.CreateClient(Policies.CanModifyStoreSettings);
|
||||
var guestClient = await guest.CreateClient(Policies.CanModifyStoreSettings);
|
||||
|
||||
//test no access to api when unrelated to store at all
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.GetStoreUsers(user.StoreId));
|
||||
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 AssertPermissionError(Policies.CanViewStoreSettings, async () => await managerClient.GetStore(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await managerClient.GetStoreUsers(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await managerClient.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await managerClient.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await employeeClient.GetStore(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await employeeClient.GetStoreUsers(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await employeeClient.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await employeeClient.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await guestClient.GetStore(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await guestClient.GetStoreUsers(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await guestClient.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await guestClient.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = guestRole.Id, UserId = user2.UserId });
|
||||
// add users to store
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = managerRole.Id, UserId = manager.UserId });
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = employeeRole.Id, UserId = employee.UserId });
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = guestRole.Id, UserId = guest.UserId });
|
||||
|
||||
//test no access to api when only a guest
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.GetStoreUsers(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
//test no access to api for employee
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await employeeClient.GetStore(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await employeeClient.GetStoreUsers(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await employeeClient.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await employeeClient.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
|
||||
//test no access to api for guest
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await guestClient.GetStore(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await guestClient.GetStoreUsers(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await guestClient.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await guestClient.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
|
||||
//test access to api for manager
|
||||
await managerClient.GetStore(user.StoreId);
|
||||
await managerClient.GetStoreUsers(user.StoreId);
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await managerClient.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await managerClient.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
|
||||
await user2Client.GetStore(user.StoreId);
|
||||
// updates
|
||||
await client.RemoveStoreUser(user.StoreId, employee.UserId);
|
||||
await AssertHttpError(403, async () => await employeeClient.GetStore(user.StoreId));
|
||||
|
||||
await client.RemoveStoreUser(user.StoreId, user2.UserId);
|
||||
await AssertHttpError(403, async () =>
|
||||
await user2Client.GetStore(user.StoreId));
|
||||
|
||||
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = ownerRole.Id, UserId = user2.UserId });
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = ownerRole.Id, UserId = employee.UserId });
|
||||
await AssertAPIError("duplicate-store-user-role", async () =>
|
||||
await client.AddStoreUser(user.StoreId,
|
||||
new StoreUserData() { Role = ownerRole.Id, UserId = user2.UserId }));
|
||||
await user2Client.RemoveStoreUser(user.StoreId, user.UserId);
|
||||
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = ownerRole.Id, UserId = employee.UserId }));
|
||||
await employeeClient.RemoveStoreUser(user.StoreId, user.UserId);
|
||||
|
||||
//test no access to api when unrelated to store at all
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await client.GetStoreUsers(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await client.GetStore(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await client.GetStoreUsers(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await client.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await client.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
|
||||
await AssertAPIError("store-user-role-orphaned", async () => await user2Client.RemoveStoreUser(user.StoreId, user2.UserId));
|
||||
await AssertAPIError("store-user-role-orphaned", async () => await employeeClient.RemoveStoreUser(user.StoreId, employee.UserId));
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
|
@ -90,7 +90,6 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public void PayInvoice(bool mine = false, decimal? amount = null)
|
||||
{
|
||||
|
||||
if (amount is not null)
|
||||
{
|
||||
Driver.FindElement(By.Id("test-payment-amount")).Clear();
|
||||
@ -98,12 +97,12 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
Driver.WaitUntilAvailable(By.Id("FakePayment"));
|
||||
Driver.FindElement(By.Id("FakePayment")).Click();
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
Driver.WaitForElement(By.Id("CheatSuccessMessage"));
|
||||
});
|
||||
if (mine)
|
||||
{
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
Driver.WaitForElement(By.Id("CheatSuccessMessage"));
|
||||
});
|
||||
MineBlockOnInvoiceCheckout();
|
||||
}
|
||||
}
|
||||
@ -408,15 +407,12 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public void Logout()
|
||||
{
|
||||
if (!Driver.PageSource.Contains("id=\"Nav-Logout\""))
|
||||
{
|
||||
Driver.Navigate().GoToUrl(ServerUri);
|
||||
}
|
||||
if (!Driver.PageSource.Contains("id=\"Nav-Logout\"")) GoToUrl("/account");
|
||||
Driver.FindElement(By.Id("Nav-Account")).Click();
|
||||
Driver.FindElement(By.Id("Nav-Logout")).Click();
|
||||
}
|
||||
|
||||
public void LogIn(string user, string password)
|
||||
public void LogIn(string user, string password = "123456")
|
||||
{
|
||||
Driver.FindElement(By.Id("Email")).SendKeys(user);
|
||||
Driver.FindElement(By.Id("Password")).SendKeys(password);
|
||||
@ -645,5 +641,38 @@ retry:
|
||||
Driver.FindElement(By.Id($"SectionNav-{navPages}")).Click();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddUserToStore(string storeId, string email, string role)
|
||||
{
|
||||
if (Driver.FindElements(By.Id("AddUser")).Count == 0)
|
||||
{
|
||||
GoToStore(storeId, StoreNavPages.Users);
|
||||
}
|
||||
Driver.FindElement(By.Id("Email")).SendKeys(email);
|
||||
new SelectElement(Driver.FindElement(By.Id("Role"))).SelectByValue(role);
|
||||
Driver.FindElement(By.Id("AddUser")).Click();
|
||||
Assert.Contains("User added successfully", FindAlertMessage().Text);
|
||||
}
|
||||
|
||||
public void AssertPageAccess(bool shouldHaveAccess, string url)
|
||||
{
|
||||
GoToUrl(url);
|
||||
Assert.DoesNotMatch("404 - Page not found</h", Driver.PageSource);
|
||||
if (shouldHaveAccess)
|
||||
Assert.DoesNotMatch("- Denied</h", Driver.PageSource);
|
||||
else
|
||||
Assert.Contains("- Denied</h", Driver.PageSource);
|
||||
}
|
||||
|
||||
public (string appName, string appId) CreateApp(string type, string name = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) name = $"{type}-{Guid.NewGuid().ToString()[..14]}";
|
||||
Driver.FindElement(By.Id($"StoreNav-Create{type}")).Click();
|
||||
Driver.FindElement(By.Name("AppName")).SendKeys(name);
|
||||
Driver.FindElement(By.Id("Create")).Click();
|
||||
Assert.Contains("App successfully created", FindAlertMessage().Text);
|
||||
var appId = Driver.Url.Split('/')[4];
|
||||
return (name, appId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
@ -14,10 +13,8 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
@ -27,7 +24,6 @@ using BTCPayServer.Views.Manage;
|
||||
using BTCPayServer.Views.Server;
|
||||
using BTCPayServer.Views.Stores;
|
||||
using BTCPayServer.Views.Wallets;
|
||||
using Dapper;
|
||||
using ExchangeSharp;
|
||||
using LNURL;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@ -42,7 +38,6 @@ using OpenQA.Selenium.Support.Extensions;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -86,8 +81,9 @@ namespace BTCPayServer.Tests
|
||||
s.GenerateWallet(isHotWallet: true);
|
||||
|
||||
// Point Of Sale
|
||||
var appName = $"PoS-{Guid.NewGuid().ToString()[..21]}";
|
||||
s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click();
|
||||
s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString());
|
||||
s.Driver.FindElement(By.Id("AppName")).SendKeys(appName);
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
|
||||
|
||||
@ -349,17 +345,18 @@ namespace BTCPayServer.Tests
|
||||
s.GoToHome();
|
||||
|
||||
//Change Password & Log Out
|
||||
var newPassword = "abc???";
|
||||
s.GoToProfile(ManageNavPages.ChangePassword);
|
||||
s.Driver.FindElement(By.Id("OldPassword")).SendKeys("123456");
|
||||
s.Driver.FindElement(By.Id("NewPassword")).SendKeys("abc???");
|
||||
s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("abc???");
|
||||
s.Driver.FindElement(By.Id("NewPassword")).SendKeys(newPassword);
|
||||
s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys(newPassword);
|
||||
s.Driver.FindElement(By.Id("UpdatePassword")).Click();
|
||||
s.Logout();
|
||||
s.Driver.AssertNoError();
|
||||
|
||||
//Log In With New Password
|
||||
s.Driver.FindElement(By.Id("Email")).SendKeys(email);
|
||||
s.Driver.FindElement(By.Id("Password")).SendKeys("abc???");
|
||||
s.Driver.FindElement(By.Id("Password")).SendKeys(newPassword);
|
||||
s.Driver.FindElement(By.Id("LoginButton")).Click();
|
||||
|
||||
s.GoToHome();
|
||||
@ -383,11 +380,14 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.Navigate().GoToUrl(url);
|
||||
Assert.Equal("hidden", s.Driver.FindElement(By.Id("Email")).GetAttribute("type"));
|
||||
Assert.Equal(usr, s.Driver.FindElement(By.Id("Email")).GetAttribute("value"));
|
||||
Assert.Equal("Create Account", s.Driver.FindElement(By.CssSelector("h4")).Text);
|
||||
Assert.Contains("Invitation accepted. Please set your password.", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Info).Text);
|
||||
|
||||
s.Driver.FindElement(By.Id("Password")).SendKeys("123456");
|
||||
s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("123456");
|
||||
s.Driver.FindElement(By.Id("SetPassword")).Click();
|
||||
s.FindAlertMessage();
|
||||
Assert.Contains("Account successfully created.", s.FindAlertMessage().Text);
|
||||
|
||||
s.Driver.FindElement(By.Id("Email")).SendKeys(usr);
|
||||
s.Driver.FindElement(By.Id("Password")).SendKeys("123456");
|
||||
s.Driver.FindElement(By.Id("LoginButton")).Click();
|
||||
@ -428,11 +428,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("Policies updated successfully", s.FindAlertMessage().Text);
|
||||
Assert.True(s.Driver.FindElement(By.Id("RequiresUserApproval")).Selected);
|
||||
|
||||
// Check user create view has approval checkbox
|
||||
s.GoToServer(ServerNavPages.Users);
|
||||
s.Driver.FindElement(By.Id("CreateUser")).Click();
|
||||
Assert.False(s.Driver.FindElement(By.Id("Approved")).Selected);
|
||||
|
||||
// Ensure there is no unread notification yet
|
||||
s.Driver.ElementDoesNotExist(By.Id("NotificationsBadge"));
|
||||
s.Logout();
|
||||
@ -933,11 +928,9 @@ namespace BTCPayServer.Tests
|
||||
s.GoToHome();
|
||||
s.Logout();
|
||||
|
||||
// Let's add Bob as a guest to alice's store
|
||||
// Let's add Bob as an employee to alice's store
|
||||
s.LogIn(alice);
|
||||
s.GoToUrl(storeUrl + "/users");
|
||||
s.Driver.FindElement(By.Id("Email")).SendKeys(bob + Keys.Enter);
|
||||
Assert.Contains("User added successfully", s.Driver.PageSource);
|
||||
s.AddUserToStore(storeId, bob, "Employee");
|
||||
s.Logout();
|
||||
|
||||
// Bob should not have access to store, but should have access to invoice
|
||||
@ -998,7 +991,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var storeLink = s.Driver.FindElement(By.Id($"Store-{storeId}"));
|
||||
Assert.Contains(storeName, storeLink.Text);
|
||||
storeLink.Click();
|
||||
s.GoToStore(storeId);
|
||||
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
|
||||
Assert.Contains("The store has been unarchived and will appear in the stores list by default again.", s.FindAlertMessage().Text);
|
||||
}
|
||||
@ -1068,7 +1061,8 @@ namespace BTCPayServer.Tests
|
||||
Policies.CanViewInvoices,
|
||||
Policies.CanModifyInvoices,
|
||||
Policies.CanViewPaymentRequests,
|
||||
Policies.CanViewStoreSettings,
|
||||
Policies.CanViewPullPayments,
|
||||
Policies.CanViewPayouts,
|
||||
Policies.CanModifyStoreSettingsUnscoped,
|
||||
Policies.CanDeleteUser
|
||||
});
|
||||
@ -1154,24 +1148,21 @@ namespace BTCPayServer.Tests
|
||||
await s.StartAsync();
|
||||
var userId = s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click();
|
||||
s.Driver.FindElement(By.Name("AppName")).SendKeys("PoS" + Guid.NewGuid());
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
|
||||
|
||||
(_, string appId) = s.CreateApp("PointOfSale");
|
||||
s.Driver.FindElement(By.Id("Title")).Clear();
|
||||
s.Driver.FindElement(By.Id("Title")).SendKeys("Tea shop");
|
||||
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Cart']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".template-item:nth-of-type(1)")).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("ApplyItemChanges")).Click();
|
||||
|
||||
s.Driver.ScrollTo(By.Id("CodeTabButton"));
|
||||
s.Driver.FindElement(By.Id("CodeTabButton")).Click();
|
||||
var template = s.Driver.FindElement(By.Id("TemplateConfig")).GetAttribute("value");
|
||||
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);
|
||||
var appId = s.Driver.Url.Split('/')[4];
|
||||
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
var windows = s.Driver.WindowHandles;
|
||||
@ -1270,11 +1261,8 @@ namespace BTCPayServer.Tests
|
||||
s.CreateNewStore();
|
||||
s.AddDerivationScheme();
|
||||
|
||||
s.Driver.FindElement(By.Id("StoreNav-CreateCrowdfund")).Click();
|
||||
s.Driver.FindElement(By.Name("AppName")).SendKeys("CF" + Guid.NewGuid());
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
|
||||
|
||||
(_, string appId) = s.CreateApp("Crowdfund");
|
||||
s.Driver.FindElement(By.Id("Title")).Clear();
|
||||
s.Driver.FindElement(By.Id("Title")).SendKeys("Kukkstarter");
|
||||
s.Driver.FindElement(By.CssSelector("div.note-editable.card-block")).SendKeys("1BTC = 1BTC");
|
||||
s.Driver.FindElement(By.Id("TargetCurrency")).Clear();
|
||||
@ -1293,7 +1281,6 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
var editUrl = s.Driver.Url;
|
||||
var appId = editUrl.Split('/')[4];
|
||||
|
||||
// Check public page
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
@ -1370,9 +1357,16 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.ScrollTo(By.Id("btAddItem"));
|
||||
s.Driver.FindElement(By.Id("btAddItem")).Click();
|
||||
s.Driver.FindElement(By.Id("EditorTitle")).SendKeys("Perk 1");
|
||||
s.Driver.FindElement(By.Id("EditorId")).SendKeys("Perk-1");
|
||||
s.Driver.FindElement(By.Id("EditorAmount")).SendKeys("20");
|
||||
s.Driver.FindElement(By.Id("ApplyItemChanges")).Click();
|
||||
// Test autogenerated ID
|
||||
Assert.Equal("perk-1", s.Driver.FindElement(By.Id("EditorId")).GetAttribute("value"));
|
||||
s.Driver.FindElement(By.Id("EditorId")).Clear();
|
||||
s.Driver.FindElement(By.Id("EditorId")).SendKeys("Perk-1");
|
||||
s.Driver.ScrollTo(By.Id("CodeTabButton"));
|
||||
s.Driver.FindElement(By.Id("CodeTabButton")).Click();
|
||||
var template = s.Driver.FindElement(By.Id("TemplateConfig")).GetAttribute("value");
|
||||
Assert.Contains("\"title\": \"Perk 1\"", template);
|
||||
Assert.Contains("\"id\": \"Perk-1\"", template);
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
@ -2499,7 +2493,6 @@ namespace BTCPayServer.Tests
|
||||
using var s = CreateSeleniumTester();
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
|
||||
s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
@ -2507,10 +2500,7 @@ namespace BTCPayServer.Tests
|
||||
s.AddLightningNode(LightningConnectionType.CLightning, false);
|
||||
s.GoToLightningSettings();
|
||||
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), true);
|
||||
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.CreateApp("PointOfSale");
|
||||
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Print']")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
@ -2529,13 +2519,10 @@ 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();
|
||||
|
||||
// Create users
|
||||
var user = s.RegisterNewUser();
|
||||
@ -2546,24 +2533,16 @@ namespace BTCPayServer.Tests
|
||||
s.RegisterNewUser(true);
|
||||
|
||||
// Setup store and associate user
|
||||
s.CreateNewStore();
|
||||
(_, string storeId) = s.CreateNewStore();
|
||||
s.GoToStore();
|
||||
s.AddLightningNode(LightningConnectionType.CLightning, false);
|
||||
s.GoToStore(StoreNavPages.Users);
|
||||
s.Driver.FindElement(By.Id("Email")).Clear();
|
||||
s.Driver.FindElement(By.Id("Email")).SendKeys(user);
|
||||
new SelectElement(s.Driver.FindElement(By.Id("Role"))).SelectByValue("Guest");
|
||||
s.Driver.FindElement(By.Id("AddUser")).Click();
|
||||
Assert.Contains("User added successfully", s.FindAlertMessage().Text);
|
||||
s.AddDerivationScheme();
|
||||
s.AddUserToStore(storeId, user, "Guest");
|
||||
|
||||
// Setup POS
|
||||
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.CreateApp("PointOfSale");
|
||||
var editUrl = s.Driver.Url;
|
||||
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Light']")).Click();
|
||||
s.Driver.FindElement(By.Id("Currency")).SendKeys("EUR");
|
||||
|
||||
Assert.False(s.Driver.FindElement(By.Id("EnableTips")).Selected);
|
||||
s.Driver.FindElement(By.Id("EnableTips")).Click();
|
||||
Assert.True(s.Driver.FindElement(By.Id("EnableTips")).Selected);
|
||||
@ -2571,9 +2550,12 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("CustomTipPercentages")).Clear();
|
||||
s.Driver.FindElement(By.Id("CustomTipPercentages")).SendKeys("10,21");
|
||||
Assert.False(s.Driver.FindElement(By.Id("ShowDiscount")).Selected);
|
||||
Assert.False(s.Driver.FindElement(By.Id("ShowItems")).Selected);
|
||||
s.Driver.FindElement(By.Id("ShowDiscount")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
// View
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
var windows = s.Driver.WindowHandles;
|
||||
Assert.Equal(2, windows.Count);
|
||||
@ -2583,6 +2565,7 @@ namespace BTCPayServer.Tests
|
||||
// basic checks
|
||||
var keypadUrl = s.Driver.Url;
|
||||
s.Driver.FindElement(By.Id("RecentTransactionsToggle"));
|
||||
s.Driver.ElementDoesNotExist(By.Id("ItemsListToggle"));
|
||||
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);
|
||||
@ -2628,6 +2611,86 @@ namespace BTCPayServer.Tests
|
||||
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);
|
||||
s.PayInvoice(true);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.MineBlockOnInvoiceCheckout();
|
||||
Assert.True(s.Driver.WaitForElement(By.Id("settled")).Displayed);
|
||||
});
|
||||
|
||||
// Receipt
|
||||
s.Driver.WaitForElement(By.Id("ReceiptLink")).Click();
|
||||
var additionalData = s.Driver.FindElement(By.CssSelector("#AdditionalData table"));
|
||||
var items = additionalData.FindElements(By.CssSelector("tbody tr"));
|
||||
var sums = additionalData.FindElements(By.CssSelector("tfoot tr"));
|
||||
Assert.Equal(2, items.Count);
|
||||
Assert.Equal(4, sums.Count);
|
||||
Assert.Contains("Manual entry 1", items[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("1 234,00 €", items[0].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Manual entry 2", items[1].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("0,56 €", items[1].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Subtotal", sums[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("1 234,56 €", sums[0].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Discount", sums[1].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("10% = 123,46 €", sums[1].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Tip", sums[2].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("10% = 111,11 €", sums[2].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Total", sums[3].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("1 222,21 €", sums[3].FindElement(By.CssSelector("td")).Text);
|
||||
|
||||
// Once more with items
|
||||
s.GoToUrl(editUrl);
|
||||
s.Driver.FindElement(By.Id("ShowItems")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
s.GoToUrl(keypadUrl);
|
||||
s.Driver.WaitForElement(By.ClassName("keypad"));
|
||||
s.Driver.FindElement(By.Id("ItemsListToggle")).Click();
|
||||
Thread.Sleep(250);
|
||||
Assert.True(s.Driver.WaitForElement(By.Id("PosItems")).Displayed);
|
||||
s.Driver.FindElement(By.CssSelector("#PosItems .posItem--displayed:nth-child(1) .btn-plus")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("#PosItems .posItem--displayed:nth-child(1) .btn-plus")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("#PosItems .posItem--displayed:nth-child(2) .btn-plus")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("#ItemsListOffcanvas button[data-bs-dismiss=\"offcanvas\"]")).Click();
|
||||
|
||||
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();
|
||||
Assert.Contains("4,23", s.Driver.FindElement(By.Id("Amount")).Text);
|
||||
Assert.Contains("2 x Green Tea (1,00 €) = 2,00 € + 1 x Black Tea (1,00 €) = 1,00 € + 1,23 €", 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("4,23 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
|
||||
s.PayInvoice(true);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.MineBlockOnInvoiceCheckout();
|
||||
Assert.True(s.Driver.WaitForElement(By.Id("settled")).Displayed);
|
||||
});
|
||||
|
||||
// Receipt
|
||||
s.Driver.WaitForElement(By.Id("ReceiptLink")).Click();
|
||||
|
||||
additionalData = s.Driver.FindElement(By.CssSelector("#AdditionalData table"));
|
||||
items = additionalData.FindElements(By.CssSelector("tbody tr"));
|
||||
sums = additionalData.FindElements(By.CssSelector("tfoot tr"));
|
||||
Assert.Equal(3, items.Count);
|
||||
Assert.Equal(2, sums.Count);
|
||||
Assert.Contains("Black Tea", items[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("1 x 1,00 € = 1,00 €", items[0].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Green Tea", items[1].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("2 x 1,00 € = 2,00 €", items[1].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Manual entry 1", items[2].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("1,23 €", items[2].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Subtotal", sums[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("4,23 €", sums[0].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Total", sums[1].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("4,23 €", sums[1].FindElement(By.CssSelector("td")).Text);
|
||||
|
||||
// Guest user can access recent transactions
|
||||
s.GoToHome();
|
||||
@ -2645,22 +2708,27 @@ namespace BTCPayServer.Tests
|
||||
|
||||
[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();
|
||||
|
||||
|
||||
// Create users
|
||||
var user = s.RegisterNewUser();
|
||||
var userAccount = s.AsTestAccount();
|
||||
s.GoToHome();
|
||||
s.Logout();
|
||||
s.GoToRegister();
|
||||
s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
|
||||
// Setup store and associate user
|
||||
(_, string storeId) = 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.AddDerivationScheme();
|
||||
s.AddUserToStore(storeId, user, "Guest");
|
||||
|
||||
// Setup POS
|
||||
s.CreateApp("PointOfSale");
|
||||
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Cart']")).Click();
|
||||
s.Driver.FindElement(By.Id("Currency")).SendKeys("EUR");
|
||||
Assert.False(s.Driver.FindElement(By.Id("EnableTips")).Selected);
|
||||
@ -2673,6 +2741,8 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("ShowDiscount")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
// View
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
var windows = s.Driver.WindowHandles;
|
||||
Assert.Equal(2, windows.Count);
|
||||
@ -2758,11 +2828,59 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("9,90 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
|
||||
|
||||
// Pay
|
||||
s.PayInvoice();
|
||||
s.PayInvoice(true);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.MineBlockOnInvoiceCheckout();
|
||||
Assert.True(s.Driver.WaitForElement(By.Id("settled")).Displayed);
|
||||
});
|
||||
|
||||
// Receipt
|
||||
s.Driver.WaitForElement(By.Id("ReceiptLink")).Click();
|
||||
var additionalData = s.Driver.FindElement(By.CssSelector("#AdditionalData table"));
|
||||
var items = additionalData.FindElements(By.CssSelector("tbody tr"));
|
||||
var sums = additionalData.FindElements(By.CssSelector("tfoot tr"));
|
||||
Assert.Equal(7, items.Count);
|
||||
Assert.Equal(4, sums.Count);
|
||||
Assert.Contains("Black Tea", items[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("2 x 1,00 € = 2,00 €", items[0].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Green Tea", items[1].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("1 x 1,00 € = 1,00 €", items[1].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Rooibos (limited)", items[2].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("2 x 1,20 € = 2,40 €", items[2].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Herbal Tea (minimum) (1,80 €)", items[3].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("1 x 1,80 € = 1,80 €", items[3].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Herbal Tea (minimum) (2,30 €)", items[4].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("1 x 2,30 € = 2,30 €", items[4].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Fruit Tea (any amount) (0,20 €)", items[5].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("1 x 0,20 € = 0,20 €", items[5].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Fruit Tea (any amount) (0,30 €)", items[6].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("1 x 0,30 € = 0,30 €", items[6].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Subtotal", sums[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("10,00 €", sums[0].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Discount", sums[1].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("10% = 1,00 €", sums[1].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Tip", sums[2].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("10% = 0,90 €", sums[2].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Total", sums[3].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("9,90 €", sums[3].FindElement(By.CssSelector("td")).Text);
|
||||
|
||||
// Check inventory got updated and is now 3 instead of 5
|
||||
s.Driver.Navigate().GoToUrl(posUrl);
|
||||
Assert.Equal("3 left", s.Driver.FindElement(By.CssSelector(".posItem:nth-child(3) .badge.inventory")).Text);
|
||||
Assert.Equal("3 left", s.Driver.FindElement(By.CssSelector(".posItem:nth-child(3) .badge.inventory")).Text);
|
||||
|
||||
// Guest user can access recent transactions
|
||||
s.GoToHome();
|
||||
s.Logout();
|
||||
s.LogIn(user, userAccount.RegisterDetails.Password);
|
||||
s.GoToUrl(posUrl);
|
||||
s.Driver.FindElement(By.Id("RecentTransactionsToggle"));
|
||||
s.GoToHome();
|
||||
s.Logout();
|
||||
|
||||
// Unauthenticated user can't access recent transactions
|
||||
s.GoToUrl(posUrl);
|
||||
s.Driver.ElementDoesNotExist(By.Id("RecentTransactionsToggle"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -3209,6 +3327,7 @@ retry:
|
||||
Assert.StartsWith(s.ServerUri.ToString(), s.Driver.Url);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
public async Task CanUseRoleManager()
|
||||
@ -3219,8 +3338,10 @@ retry:
|
||||
s.GoToHome();
|
||||
s.GoToServer(ServerNavPages.Roles);
|
||||
var existingServerRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
|
||||
Assert.Equal(3, existingServerRoles.Count);
|
||||
Assert.Equal(5, existingServerRoles.Count);
|
||||
IWebElement ownerRow = null;
|
||||
IWebElement managerRow = null;
|
||||
IWebElement employeeRow = null;
|
||||
IWebElement guestRow = null;
|
||||
foreach (var roleItem in existingServerRoles)
|
||||
{
|
||||
@ -3228,6 +3349,14 @@ retry:
|
||||
{
|
||||
ownerRow = roleItem;
|
||||
}
|
||||
else if (roleItem.Text.Contains("manager", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
managerRow = roleItem;
|
||||
}
|
||||
else if (roleItem.Text.Contains("employee", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
employeeRow = roleItem;
|
||||
}
|
||||
else if (roleItem.Text.Contains("guest", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
guestRow = roleItem;
|
||||
@ -3235,11 +3364,21 @@ retry:
|
||||
}
|
||||
|
||||
Assert.NotNull(ownerRow);
|
||||
Assert.NotNull(managerRow);
|
||||
Assert.NotNull(employeeRow);
|
||||
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 managerBadges = managerRow.FindElements(By.CssSelector(".badge"));
|
||||
Assert.DoesNotContain(managerBadges, element => element.Text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
|
||||
Assert.Contains(managerBadges, element => element.Text.Equals("Server-wide", StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
var employeeBadges = employeeRow.FindElements(By.CssSelector(".badge"));
|
||||
Assert.DoesNotContain(employeeBadges, element => element.Text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
|
||||
Assert.Contains(employeeBadges, 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));
|
||||
@ -3267,13 +3406,11 @@ retry:
|
||||
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)));
|
||||
Assert.Equal(5, existingStoreRoles.Count);
|
||||
Assert.Equal(4, existingStoreRoles.Count(element => element.Text.Contains("Server-wide", StringComparison.InvariantCultureIgnoreCase)));
|
||||
|
||||
foreach (var roleItem in existingStoreRoles)
|
||||
{
|
||||
@ -3324,20 +3461,19 @@ retry:
|
||||
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.Equal(4, 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(4, existingStoreRoles.Count);
|
||||
Assert.Equal(3, 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.Equal(3, options.Count);
|
||||
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");
|
||||
@ -3349,6 +3485,260 @@ retry:
|
||||
Assert.Contains("Malice",s.Driver.PageSource);
|
||||
Assert.DoesNotContain(Policies.CanModifyServerSettings,s.Driver.PageSource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
public async Task CanAccessUserStoreAsAdmin()
|
||||
{
|
||||
using var s = CreateSeleniumTester(newDb: true);
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
var storeSettingsPaths = new [] {"settings", "rates", "checkout", "tokens", "users", "roles", "webhooks", "payout-processors", "payout-processors/onchain-automated/BTC", "payout-processors/lightning-automated/BTC", "emails", "email-settings", "forms"};
|
||||
|
||||
// Setup user, store and wallets
|
||||
s.RegisterNewUser();
|
||||
(_, string storeId) = s.CreateNewStore();
|
||||
s.GoToStore();
|
||||
s.GenerateWallet(isHotWallet: true);
|
||||
s.AddLightningNode(LightningConnectionType.CLightning, false);
|
||||
|
||||
// Add apps
|
||||
(_, string posId) = s.CreateApp("PointOfSale");
|
||||
(_, string crowdfundId) = s.CreateApp("Crowdfund");
|
||||
s.Logout();
|
||||
|
||||
// Setup admin and check access
|
||||
s.GoToRegister();
|
||||
s.RegisterNewUser(true);
|
||||
string GetStorePath(string subPath) => $"/stores/{storeId}/{subPath}";
|
||||
|
||||
// Admin access
|
||||
s.AssertPageAccess(false, GetStorePath(""));
|
||||
s.AssertPageAccess(true, GetStorePath("reports"));
|
||||
s.AssertPageAccess(true, GetStorePath("invoices"));
|
||||
s.AssertPageAccess(false, GetStorePath("invoices/create"));
|
||||
s.AssertPageAccess(true, GetStorePath("payment-requests"));
|
||||
s.AssertPageAccess(false, GetStorePath("payment-requests/edit"));
|
||||
s.AssertPageAccess(true, GetStorePath("pull-payments"));
|
||||
s.AssertPageAccess(true, GetStorePath("payouts"));
|
||||
s.AssertPageAccess(false, GetStorePath("onchain/BTC"));
|
||||
s.AssertPageAccess(false, GetStorePath("onchain/BTC/settings"));
|
||||
s.AssertPageAccess(false, GetStorePath("lightning/BTC"));
|
||||
s.AssertPageAccess(false, GetStorePath("lightning/BTC/settings"));
|
||||
s.AssertPageAccess(false, GetStorePath("apps/create"));
|
||||
foreach (var path in storeSettingsPaths)
|
||||
{ // should have view access to settings, but no submit buttons or create links
|
||||
TestLogs.LogInformation($"Checking access to store page {path} as admin");
|
||||
s.AssertPageAccess(true, $"stores/{storeId}/{path}");
|
||||
if (path != "payout-processors")
|
||||
{
|
||||
s.Driver.ElementDoesNotExist(By.CssSelector("#mainContent .btn-primary"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
public async Task CanUsePredefinedRoles()
|
||||
{
|
||||
using var s = CreateSeleniumTester(newDb: true);
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
var storeSettingsPaths = new [] {"settings", "rates", "checkout", "tokens", "users", "roles", "webhooks", "payout-processors", "payout-processors/onchain-automated/BTC", "payout-processors/lightning-automated/BTC", "emails", "email-settings", "forms"};
|
||||
|
||||
// Setup users
|
||||
var manager = s.RegisterNewUser();
|
||||
s.Logout();
|
||||
s.GoToRegister();
|
||||
var employee = s.RegisterNewUser();
|
||||
s.Logout();
|
||||
s.GoToRegister();
|
||||
var guest = s.RegisterNewUser();
|
||||
s.Logout();
|
||||
s.GoToRegister();
|
||||
|
||||
// Setup store, wallets and add users
|
||||
s.RegisterNewUser(true);
|
||||
(_, string storeId) = s.CreateNewStore();
|
||||
s.GoToStore();
|
||||
s.GenerateWallet(isHotWallet: true);
|
||||
s.AddLightningNode(LightningConnectionType.CLightning, false);
|
||||
s.AddUserToStore(storeId, manager, "Manager");
|
||||
s.AddUserToStore(storeId, employee, "Employee");
|
||||
s.AddUserToStore(storeId, guest, "Guest");
|
||||
|
||||
// Add apps
|
||||
(_, string posId) = s.CreateApp("PointOfSale");
|
||||
(_, string crowdfundId) = s.CreateApp("Crowdfund");
|
||||
|
||||
string GetStorePath(string subPath) => $"/stores/{storeId}/{subPath}";
|
||||
|
||||
// Owner access
|
||||
s.AssertPageAccess(true, GetStorePath(""));
|
||||
s.AssertPageAccess(true, GetStorePath("reports"));
|
||||
s.AssertPageAccess(true, GetStorePath("invoices"));
|
||||
s.AssertPageAccess(true, GetStorePath("invoices/create"));
|
||||
s.AssertPageAccess(true, GetStorePath("payment-requests"));
|
||||
s.AssertPageAccess(true, GetStorePath("payment-requests/edit"));
|
||||
s.AssertPageAccess(true, GetStorePath("pull-payments"));
|
||||
s.AssertPageAccess(true, GetStorePath("payouts"));
|
||||
s.AssertPageAccess(true, GetStorePath("onchain/BTC"));
|
||||
s.AssertPageAccess(true, GetStorePath("onchain/BTC/settings"));
|
||||
s.AssertPageAccess(true, GetStorePath("lightning/BTC"));
|
||||
s.AssertPageAccess(true, GetStorePath("lightning/BTC/settings"));
|
||||
s.AssertPageAccess(true, GetStorePath("apps/create"));
|
||||
s.AssertPageAccess(true, $"/apps/{posId}/settings/pos");
|
||||
s.AssertPageAccess(true, $"/apps/{crowdfundId}/settings/crowdfund");
|
||||
foreach (var path in storeSettingsPaths)
|
||||
{ // should have manage access to settings, hence should see submit buttons or create links
|
||||
TestLogs.LogInformation($"Checking access to store page {path} as owner");
|
||||
s.AssertPageAccess(true, $"stores/{storeId}/{path}");
|
||||
if (path != "payout-processors")
|
||||
{
|
||||
s.Driver.FindElement(By.CssSelector("#mainContent .btn-primary"));
|
||||
}
|
||||
}
|
||||
s.Logout();
|
||||
|
||||
// Manager access
|
||||
s.LogIn(manager);
|
||||
s.AssertPageAccess(false, GetStorePath(""));
|
||||
s.AssertPageAccess(true, GetStorePath("reports"));
|
||||
s.AssertPageAccess(true, GetStorePath("invoices"));
|
||||
s.AssertPageAccess(true, GetStorePath("invoices/create"));
|
||||
s.AssertPageAccess(true, GetStorePath("payment-requests"));
|
||||
s.AssertPageAccess(true, GetStorePath("payment-requests/edit"));
|
||||
s.AssertPageAccess(true, GetStorePath("pull-payments"));
|
||||
s.AssertPageAccess(true, GetStorePath("payouts"));
|
||||
s.AssertPageAccess(false, GetStorePath("onchain/BTC"));
|
||||
s.AssertPageAccess(false, GetStorePath("onchain/BTC/settings"));
|
||||
s.AssertPageAccess(false, GetStorePath("lightning/BTC"));
|
||||
s.AssertPageAccess(false, GetStorePath("lightning/BTC/settings"));
|
||||
s.AssertPageAccess(false, GetStorePath("apps/create"));
|
||||
s.AssertPageAccess(true, $"/apps/{posId}/settings/pos");
|
||||
s.AssertPageAccess(true, $"/apps/{crowdfundId}/settings/crowdfund");
|
||||
foreach (var path in storeSettingsPaths)
|
||||
{ // should have view access to settings, but no submit buttons or create links
|
||||
TestLogs.LogInformation($"Checking access to store page {path} as manager");
|
||||
s.AssertPageAccess(true, $"stores/{storeId}/{path}");
|
||||
s.Driver.ElementDoesNotExist(By.CssSelector("#mainContent .btn-primary"));
|
||||
}
|
||||
s.Logout();
|
||||
|
||||
// Employee access
|
||||
s.LogIn(employee);
|
||||
s.AssertPageAccess(false, GetStorePath(""));
|
||||
s.AssertPageAccess(false, GetStorePath("reports"));
|
||||
s.AssertPageAccess(true, GetStorePath("invoices"));
|
||||
s.AssertPageAccess(true, GetStorePath("invoices/create"));
|
||||
s.AssertPageAccess(true, GetStorePath("payment-requests"));
|
||||
s.AssertPageAccess(true, GetStorePath("payment-requests/edit"));
|
||||
s.AssertPageAccess(true, GetStorePath("pull-payments"));
|
||||
s.AssertPageAccess(true, GetStorePath("payouts"));
|
||||
s.AssertPageAccess(false, GetStorePath("onchain/BTC"));
|
||||
s.AssertPageAccess(false, GetStorePath("onchain/BTC/settings"));
|
||||
s.AssertPageAccess(false, GetStorePath("lightning/BTC"));
|
||||
s.AssertPageAccess(false, GetStorePath("lightning/BTC/settings"));
|
||||
s.AssertPageAccess(false, GetStorePath("apps/create"));
|
||||
s.AssertPageAccess(false, $"/apps/{posId}/settings/pos");
|
||||
s.AssertPageAccess(false, $"/apps/{crowdfundId}/settings/crowdfund");
|
||||
foreach (var path in storeSettingsPaths)
|
||||
{ // should not have access to settings
|
||||
TestLogs.LogInformation($"Checking access to store page {path} as employee");
|
||||
s.AssertPageAccess(false, $"stores/{storeId}/{path}");
|
||||
}
|
||||
s.Logout();
|
||||
|
||||
// Guest access
|
||||
s.LogIn(guest);
|
||||
s.AssertPageAccess(false, GetStorePath(""));
|
||||
s.AssertPageAccess(false, GetStorePath("reports"));
|
||||
s.AssertPageAccess(true, GetStorePath("invoices"));
|
||||
s.AssertPageAccess(true, GetStorePath("invoices/create"));
|
||||
s.AssertPageAccess(true, GetStorePath("payment-requests"));
|
||||
s.AssertPageAccess(false, GetStorePath("payment-requests/edit"));
|
||||
s.AssertPageAccess(true, GetStorePath("pull-payments"));
|
||||
s.AssertPageAccess(true, GetStorePath("payouts"));
|
||||
s.AssertPageAccess(false, GetStorePath("onchain/BTC"));
|
||||
s.AssertPageAccess(false, GetStorePath("onchain/BTC/settings"));
|
||||
s.AssertPageAccess(false, GetStorePath("lightning/BTC"));
|
||||
s.AssertPageAccess(false, GetStorePath("lightning/BTC/settings"));
|
||||
s.AssertPageAccess(false, GetStorePath("apps/create"));
|
||||
s.AssertPageAccess(false, $"/apps/{posId}/settings/pos");
|
||||
s.AssertPageAccess(false, $"/apps/{crowdfundId}/settings/crowdfund");
|
||||
foreach (var path in storeSettingsPaths)
|
||||
{ // should not have access to settings
|
||||
TestLogs.LogInformation($"Checking access to store page {path} as guest");
|
||||
s.AssertPageAccess(false, $"stores/{storeId}/{path}");
|
||||
}
|
||||
s.Logout();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
public async Task CanChangeUserRoles()
|
||||
{
|
||||
using var s = CreateSeleniumTester(newDb: true);
|
||||
await s.StartAsync();
|
||||
|
||||
// Setup users and store
|
||||
var employee = s.RegisterNewUser();
|
||||
s.Logout();
|
||||
s.GoToRegister();
|
||||
var owner = s.RegisterNewUser(true);
|
||||
(_, string storeId) = s.CreateNewStore();
|
||||
s.GoToStore();
|
||||
s.AddUserToStore(storeId, employee, "Employee");
|
||||
|
||||
// Should successfully change the role
|
||||
var userRows = s.Driver.FindElements(By.CssSelector("#StoreUsersList tr"));
|
||||
Assert.Equal(2, userRows.Count);
|
||||
IWebElement employeeRow = null;
|
||||
foreach (var row in userRows)
|
||||
{
|
||||
if (row.Text.Contains(employee, StringComparison.InvariantCultureIgnoreCase)) employeeRow = row;
|
||||
}
|
||||
Assert.NotNull(employeeRow);
|
||||
employeeRow.FindElement(By.CssSelector("a[data-bs-target=\"#EditModal\"]")).Click();
|
||||
Assert.Equal(s.Driver.WaitForElement(By.Id("EditUserEmail")).Text, employee);
|
||||
new SelectElement(s.Driver.FindElement(By.Id("EditUserRole"))).SelectByValue("Manager");
|
||||
s.Driver.FindElement(By.Id("EditContinue")).Click();
|
||||
Assert.Contains($"The role of {employee} has been changed to Manager.", s.FindAlertMessage().Text);
|
||||
|
||||
// Should not see a message when not changing role
|
||||
userRows = s.Driver.FindElements(By.CssSelector("#StoreUsersList tr"));
|
||||
Assert.Equal(2, userRows.Count);
|
||||
employeeRow = null;
|
||||
foreach (var row in userRows)
|
||||
{
|
||||
if (row.Text.Contains(employee, StringComparison.InvariantCultureIgnoreCase)) employeeRow = row;
|
||||
}
|
||||
Assert.NotNull(employeeRow);
|
||||
employeeRow.FindElement(By.CssSelector("a[data-bs-target=\"#EditModal\"]")).Click();
|
||||
Assert.Equal(s.Driver.WaitForElement(By.Id("EditUserEmail")).Text, employee);
|
||||
// no change, no alert message
|
||||
s.Driver.FindElement(By.Id("EditContinue")).Click();
|
||||
s.Driver.ElementDoesNotExist(By.CssSelector("#mainContent .alert"));
|
||||
|
||||
// Should not change last owner
|
||||
userRows = s.Driver.FindElements(By.CssSelector("#StoreUsersList tr"));
|
||||
Assert.Equal(2, userRows.Count);
|
||||
IWebElement ownerRow = null;
|
||||
foreach (var row in userRows)
|
||||
{
|
||||
if (row.Text.Contains(owner, StringComparison.InvariantCultureIgnoreCase)) ownerRow = row;
|
||||
}
|
||||
Assert.NotNull(ownerRow);
|
||||
ownerRow.FindElement(By.CssSelector("a[data-bs-target=\"#EditModal\"]")).Click();
|
||||
Assert.Equal(s.Driver.WaitForElement(By.Id("EditUserEmail")).Text, owner);
|
||||
new SelectElement(s.Driver.FindElement(By.Id("EditUserRole"))).SelectByValue("Employee");
|
||||
s.Driver.FindElement(By.Id("EditContinue")).Click();
|
||||
Assert.Contains($"User {owner} is the last owner. Their role cannot be changed.", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error).Text);
|
||||
}
|
||||
|
||||
private static void CanBrowseContent(SeleniumTester s)
|
||||
{
|
||||
|
@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
@ -25,6 +27,7 @@ using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.CodeAnalysis.Operations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Payment;
|
||||
using NBitpayClient;
|
||||
@ -32,6 +35,7 @@ using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Npgsql;
|
||||
using Xunit;
|
||||
using Xunit.Sdk;
|
||||
|
||||
@ -550,13 +554,23 @@ retry:
|
||||
|
||||
public async Task AddGuest(string userId)
|
||||
{
|
||||
var repo = this.parent.PayTester.GetService<StoreRepository>();
|
||||
await repo.AddStoreUser(StoreId, userId, StoreRoleId.Guest);
|
||||
var repo = parent.PayTester.GetService<StoreRepository>();
|
||||
await repo.AddOrUpdateStoreUser(StoreId, userId, StoreRoleId.Guest);
|
||||
}
|
||||
public async Task AddOwner(string userId)
|
||||
{
|
||||
var repo = this.parent.PayTester.GetService<StoreRepository>();
|
||||
await repo.AddStoreUser(StoreId, userId, StoreRoleId.Owner);
|
||||
var repo = parent.PayTester.GetService<StoreRepository>();
|
||||
await repo.AddOrUpdateStoreUser(StoreId, userId, StoreRoleId.Owner);
|
||||
}
|
||||
public async Task AddManager(string userId)
|
||||
{
|
||||
var repo = parent.PayTester.GetService<StoreRepository>();
|
||||
await repo.AddOrUpdateStoreUser(StoreId, userId, StoreRoleId.Manager);
|
||||
}
|
||||
public async Task AddEmployee(string userId)
|
||||
{
|
||||
var repo = parent.PayTester.GetService<StoreRepository>();
|
||||
await repo.AddOrUpdateStoreUser(StoreId, userId, StoreRoleId.Employee);
|
||||
}
|
||||
|
||||
public async Task<uint256> PayOnChain(string invoiceId)
|
||||
@ -647,5 +661,33 @@ retry:
|
||||
LNAddress = lnAddrUser;
|
||||
return lnAddrUser;
|
||||
}
|
||||
|
||||
public async Task ImportOldInvoices(string storeId = null)
|
||||
{
|
||||
storeId ??= StoreId;
|
||||
var oldInvoices = File.ReadAllLines(TestUtils.GetTestDataFullPath("OldInvoices.csv"));
|
||||
var oldPayments = File.ReadAllLines(TestUtils.GetTestDataFullPath("OldPayments.csv"));
|
||||
var dbContext = this.parent.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
|
||||
var db = (NpgsqlConnection)dbContext.Database.GetDbConnection();
|
||||
await db.OpenAsync();
|
||||
using (var writer = db.BeginTextImport("COPY \"Invoices\" (\"Id\",\"Blob\",\"Created\",\"CustomerEmail\",\"ExceptionStatus\",\"ItemCode\",\"OrderId\",\"Status\",\"StoreDataId\",\"Archived\",\"Blob2\") FROM STDIN DELIMITER ',' CSV HEADER"))
|
||||
{
|
||||
foreach (var invoice in oldInvoices)
|
||||
{
|
||||
var localInvoice = invoice.Replace("3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd", storeId);
|
||||
await writer.WriteLineAsync(localInvoice);
|
||||
}
|
||||
await writer.FlushAsync();
|
||||
}
|
||||
using (var writer = db.BeginTextImport("COPY \"Payments\" (\"Id\",\"Blob\",\"InvoiceDataId\",\"Accounted\",\"Blob2\",\"Type\") FROM STDIN DELIMITER ',' CSV HEADER"))
|
||||
{
|
||||
foreach (var invoice in oldPayments)
|
||||
{
|
||||
var localPayment = invoice.Replace("3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd", storeId);
|
||||
await writer.WriteLineAsync(localPayment);
|
||||
}
|
||||
await writer.FlushAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
11
BTCPayServer.Tests/TestData/OldInvoices.csv
Normal file
11
BTCPayServer.Tests/TestData/OldInvoices.csv
Normal file
@ -0,0 +1,11 @@
|
||||
Id,Blob,Created,CustomerEmail,ExceptionStatus,ItemCode,OrderId,Status,StoreDataId,Archived,Blob2
|
||||
Q7RqoHLngK9svM4MgRyi9y,\x1f8b0800000000000003c454cb76a24010dde72b7258cf180d08929d0a89899ae323ea4962163c0ae808ddd8344426c72f9bc57cd2fcc2340d31ea38b398cd2cfb5657d5add7fdf9fdc7fbd9f9b9845ce9ea5c1a6b9335e90db0dfd7936ca80cfd498ef45cfa52fc4818a1702bbec9893feb76d9ace3abd3d6e06e456dd76b2fece46eb8eee4d7032f9bae5f6fd44dbfb330ddd2995017a870c6691896f16200774442e4e41cae0b8c5a0cf8436dea6a4d56344d5135596e2ac286704690030f282abe349a724bd6e5a67c298cb089117746041fd815a5b2bb109304b1b6eb524812514347ef1bb2a6cd0686663e7a8b4779dcf3bd45eb5ae9bcb181e10f50c93ca6c44d1d768b3d422391817b172d2b2831880c489c229e0d4085478577890b7be51691823c418e1572d4b3c2043e60caabe29856ab578893520a58b4459a4d0d89a35bc1c54e73dec5534c84e5de8a8e520ad88c2c149ec0bb24c58ce6272c4f283e814e59399ddfe220762a48d5ebcb3f9b1a274ca380e08f24bbbaf9ec0c8b59fbdbc3d70965a20953566c8d2fbab589535b35dab61f78e9a4bb50c76b2af7bda9f18a56caca9ca1acbdf202e7a6375ccd4d50eca775539e8fbf69fa4a514c326e0e8d7870d7efb747fd66369faba38d366b4c641dc6c8c67660240b3d35c78dbe1be85dc38efcc9d7e7f832095ea4d38c1088457b5f4a9d87ee52ba5afe277a4b69fb71c1164b05270c6f5275370ec425e7cab6eb706ce511605660cf2fe575829762d7b243d85fe10a1e1e2e19475d44c161b3c9a0c818301627571717919530a0981f0767b342d8af39242ab9b0cd3588d3ad9762e0f150f784218f1f4d41b160c2685a26c57b8632c5a7b000cd80ce68789817610cace642446a36737875e5bf1aa17e99dfa179cc48b568d55df1c9ed1e7fd7a7f296cb9e0d8105a410bbf7edcee4014c4aefc60e3baa5860ffa454c279bbbb978860c4d59a77d7dce9e24e135bf54a130354487aa14855323898bf95f18916c3aeac3d2b090e7fc0860176c13d1ed2e76a40566dd0f1563d9010a88585f0356af5b3ed2f000000ffff030035140a5d88060000,2018-10-01 11:32:12+00,,,,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
Ka6GHBrFJPRwFRga1RD6Yz,\x1f8b0800000000000003c454cd769a4014dee7297258b74682a066a7427e24e6284a729a9fc5001798083364188826c727eba28fd457e830101353db45375dcef7cdbdf7bbbf3fbfff783d383c5470a09c1c2a3632cece87ec743c759e4f9d08a98e697c7b51be543f724e195cc86f5a1eb9a31177879131ef5d8e97cc0bc2c18d978f274fc3f5e96558ce9f1ecf8c953dbcb182da98b2009834264592d4fe3280604a13ecaf05dc9618431cc44337ba46abad6aed76db50b5635d72989414fbb0c069f545d5b59ed6eff4f4da10561916c698921d5eef367c0019cd311f0401833c973998b7a5e742ef7110a7fd65e9f62889c7b6f5726b777d3fc25c9fd5ca334683c2e71724a42c9511847555b24a1287d484dcaffc9d310072b80024cd1a724403f89073e52e5ee7d84789404394e4f00633915a558656fbb881fc823120b2388ae53a8a4037529157ac452df7e991cc154a3fc594b095229cecc147b4209cadf730b738db83ce79dda3dffc60becf4953f1e33f53ea1e6a1a53f216649bb7e8a08938fa384362a870298b30e7d5ec44b25aabacf00c73e045715838a31b63f6c4343b9c9b8f78d9595a2e2e07cb30f6cfce27cb6b0b3adeed93ae5dcf5ebafd65a763d1993e31b3cbb16d0fa6b65e5e5f1bd355d7551dad0f33ec112f36f39b7e61cd543b88fb23d34b23e7eb5d769cc70fca7e4518e4b8bdde2bc3c5e85e39b9ff4ff2ee95cddb1e235e484d049e95667b7cc86acd0db7ad7086d629105e61770ff58e4258900079097c9ce1069eec0e994003ccc0e7ae7359458c39cff293a3a314e51c1811db21d42c31895a3e4d6b2d7c750a7281dbf5e686c2d515e538145b5349ac947056d441c907a20ef17e5e8095c05c96ecc6c584006f0590d296c77d915dfdaf455954c7f7d93ae3b419b466af44e7b68fbf5fa97a99eb9a4d80c7b43a79af9b2d150238b5b5bac53e652cb17fba57d278b3dd9794122c6eb6a8aeb5bd8edbcbd8d79acb18e3eab05727a909063bfd47a5e868d5ec863d4779bcfb03561c4800c1e726bd8f0694cd047d9eaa054d8021222f9fda6a1f6c7e010000ffff0300ddecc9e08e060000,2018-10-01 11:54:10+00,,,,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
Q3kZ3F8cUD57WUqcc8QLs2,\x1f8b0800000000000003c4544972da4014ddfb1494d6099110a377806463a662b0c085f142c3176a23758b564b86b83859163952ae90564b96812259649365bff77ffff9fdfaf1f3fda654929023dd96a4a9ba5da9774ddbd06a8da5b1b3ede6741855a42fa945c408850761a6461ba3db654667539f3787fd2db51cb7bdb4a2fe68d739dc0ddd64be7bbdafef079da5ee64ce843a4085338e7d3ffb2f047026c447f681c3b2c0a8c9803f6af546bd2c2baa2ccb7545add404877042900d8f28484d949ada545bd566a32248d887883b2382cff85a23e71d08498458db71284491a861921876af3f1ec361bc5bb4d427dadb57bcfba145f4fd66fe36795a6599879438b1cd1eb04b68202270efb465694a0c020d223bfd6f64460c28260e94e6ccdc22bc11feb95597e327c5a7ff7a8708d9a6cf51d7f423f88029af916395b29c23764c2960d12449376612478f22332b3ef09e5ecb4b306333b80829603d30917f05ef9218337ab8c2ac507805e545b26bff7711bbf649def9ca9f29e50a35f108fe0852d4cd27a999cc3cdd25be5c28114d98b3748736a25bfb30b6ea5adbda786e3ceb2eebd31d5507ee5c7b45dbea563750d2deba9e7ddf1b6d173a54add5aea62ea6df1bad6db5aa93696da485c3fe60d09e0c6ac962519fec1b8632535b304516b63c2d5ab6627daa0c1cafd5d5ac6033fbfa1c5622ef45ba9e1102b176ef6ba9f3d85d4bb7ebff94de5a3a7edcb3c9629113863729bf221bc22ce79c2b3a1c9a8700304bb1e797ec56c18db1635a3e9cae700e8fce978ca30ea2603363364c237a8c85d1edb76f417134517633659b04592e6c7f07e290e54c1a5cfed59830e4f2a349534c336134ce82e213220bf129334013a006f5cfe3228c81951d0848d96236af2eb32b139addad64d343c848be68f95df1c9158fbfab5576cb59cf46c03c924adffbb1a05c8059e6ad14d845c502fb27dd12cec7e25e028211d76ede5dbd50c942215b6aae901e4a053e55a43c189ccddf4cf844d361e76ccf8cbc730bd833c00e389743fa5c0d48f20dbadcaa47e20335b1103ea52cdf1c7f030000ffff03003e6b8efb96060000,2018-10-01 11:54:32+00,,,,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
FSktP1Nrxu7arh7TUFAgTZ,\x1f8b0800000000000003c4554b72e33610ddcf295c5c27b23e16297a1599923c63591efd55f1781620d1143124011a0035545c3a59163952ae10008415d9a54caab2c992af5f773ff48f7ffefec7cb878b0b8760e7fac2192d52396d3df0aaf4104fbce56ad4df2e1f9d9f344348c6e193a175c47615047275b37517bdfbbb948738ee6f42713779bed98feee3dde2f9dbad5b8d6f36435c3b338e81d7ce41a922e59f2d50872e00f0946524da2b46d3601c49501f5dd76b377a6dbfedf7bc96e7f9c646e88e91089624d79456b7d3ebf8dd66a76b8c501544391346dfda7d6bc7503041641f630e4298e70c969bd093de7886d63de1a2ea7b67ddc28fb76595dcfdba1db9eeb296597086cb487ea231e3b9c9a0bc75f5b42409f90044a4e34d9090c029c370b1902825746bfc2d2b50b862d132cb2c5a247b41229429344699805798ab376afdcd66a369b1a8e41ca82993335ccd1d851e8cb6b0dcab7a9e53662c0f287f97d4c0c31c119d56c5d54d01fe0b54282f3268442c774e99012ba9e4fb33311e497106550f97e73206449e0b62bbd1fe6753eb8c699a30fa9ae45809d5dd0192e884ae5acec9ce946521f55c6d4dfdaaa20cdd413fdc2671390f36eeec9977c6f162f08da457e9704576fd344ea2db8f93743d84abf0f1b9db59cf7ef3fcf4ea6ac866ddc9a0b8bf1b8ffbd37177b75ebbd3ca5bb5e61d1f6624a46132101bbf1cce5a639cf8c120ccb7f39fbf146d917c75ce2b226046f1e5c9b959064fcef5d3ff24efc939bcae3b92a5d144e1bb63372b82a2d66c6dc70a17689f03951afbf2b5de5f884b8a5198c1e9585b78f26f63a778987088e46a7eaf89899485b8bebc3ce15dca04d154ec59597bc86a04765dcc77acb43d304962b55a5ab4d6267959cba027861fa4504b9985284a85ad09f01df015cf4ef96a852805d9c090b3462823558a9ad760bc5e7c27e2fb42323b95762d559b8f1f3f3e77f531a80b3c0199307d465f0e47530c30afbd5b47ec5d310cf69f0e9f713e1c972b6794a8ff803a69c3e3993d9e58bf6b4f6c42f4cf429f349b0cde0c0bdaa9f6ebc9b0d68f48246f195049a018f0fbfefd3d47b0b3e3f67e04972c038e687d391bcd0f87bf000000ffff030075db901fe2060000,2018-10-01 11:57:15+00,customer@example.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
HuzCsv9hghew2FD6zVqUyY,\x1f8b0800000000000003c455cb76d33010ddf3153d5e439aa75377456aa794268126a913286521dbe358b52d3992ecc6edc997b1e093f80564590d694f8073d8b0f49d3b3357f3f28f6fdf1f5f1d1d1938304e8f8c8bfcc1e68515ad22b86f9f3be6c362ed969f8dd715830bcae0bda275f8cab56de19eadccf9c9f832665e100e961ebf9caccfcaf37158ccd777efcccde86c390c6a67ca0260b5b39dcb48e9470dd4a13380e08a26d82f25a3a9308604c88f9ed96f374eda56db3ae9b7fa7d4bd9302928f6e11aa715a5d5eb9c74ac5edfea28236c322c9d3125cfeca6a9ed01649463310802069cabe758e9f2be701d7ad3b3379f9069a72be406a865ad3fdf59e36ceab46a9919a341ee8bf724a42c5519a47755bd4a9280d401ee57f126880b60840670341728c664a5fc35cb96b864913c49349a4525c73e4a241aa284c313cce41b2bfdcd66a3a9313f670c882a933174678644b74a9b9797b29e879429cb0794be48aae0618a709556c6ad9a02ec2d6c509a25d0f0696aec336d9a13c1ca03316e707600950f178732da581c0aa2bbd1febda975c0741551f294645709d95d0709b447972d67b85065998b6aae56aa7e9b2cf74c67e0ada2309fd94b73ba669d513877ee70dc8d872e2e067118f9ef2e26f162085def66ddeb2ca60f7d2bee768774da9b38d9f872341a5c8d7ac562615e6dfa6e6bd6b1608a3de2450e5f5af970da1a0591653b5eba9abdf992b579f4d538ac08831ac5c75be3ecdabe354e6fff93bc5b63fbb4ee48e44a13817b436f960f59ad59db7615ce5099021115f6e56bbdbf10e624405e02fb63ade1c9dfc64ef202ccc017ee6c5c112321327e7a7cbcc73b161122312f695e7b88cd39e87551dfa1d4f6810a1ccad5aa4457da04cb6b1964cff087147229130ff931d7350156007359b2cf972b4408884600296d78c297a5a8790dcaeac5377c566682eaa9d46b29dbbcfbf8f3b9ab8f415de00988885667f471bb338500b3dabbb5c35e144361ff74f894f376b75c292558fe07e4491beeceac3eb1dd66b3ad4f6c84ab9f4575d2743278362ca890edaf26435b2f108f9e336023800410bcecdfaf3982428fdbcb11bca6093044eacbd968bedafe040000ffff030069b828dae2060000,2018-10-01 12:09:53+00,customer@example.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
YZV1PQasUKEXdSkSV69XDL,\x1f8b0800000000000003c455cb76db3610dde72b7cb86e65911429cbabca1463c792523d2c39759c05480c4584244003202bd5475fd6453e29bf50108455d9474dcfe9a64bdeb93373312f7efff3dbf3bbb3338b60ebf2ccfaed616dcfe648acc6e127bccc966b7ff06934b17e6a1842320e1f34cd159b5510c8d5d5c65f5e4c6e331ee164781f89dbe9d3d5eefd24a9974f5faffdedf8ea3ec4ad33e31878eb1c542a52f1ab01dad025009eb19cc43bc5e86a8c2309eac3f37da7e3f56cd7f55dcf751c6d23b46624863b523414db732fdc81ef38b636c2b624ca9930facadeb78d1d43c90491438c3908a19f33b85927f2b62f2807a7866c7ce1f5c49dbc76275e9a08e7a2ba6e65969ce12a961f68c278a13328efa67a8d2409c50844dcc49b2221815386e16c295146e846fb1b56a070c5a2559e1bb44c7782c42857688272012f30576f6cf477bb9daec1e28a73a0ba4c56b85a580add6b6d51b553f53ca54c5b3ea2e24d520d8705224d5a15b7690af05f608b8a32874ecc0aeb9819b08a4abe3b11e381942750f570792a6340e4a920a61bce3f9bec13a659cae84b92432554774748a223ba6a3927b52ecb523673b5d1f5db9655e48f86d1264daa4570efcf9fb83b4e96a3af24eb65e18ad4c32c49e3eb9b69b60ea1173d3c79ee7afe477f90f57a219b7bd35139b91d8f87b3b157afd7fe6cdb5fd90b77007312d1281d89fb4115ceed314e07c1282a368b9f3f978e48bf58a71511d0a3f8fc685ddd058fd6e5e3ff24efd1dabfac3b9295d644e177cb6c560c65abd9d80e152ed1ae002a1becf397767f21a92846510ec7636de0e9bf8d9de261c22196abc5a421a65296e2f2fcfc88772e534433b16355eb21b7efc1ac8bfe4e94b68f4c9244ad5623bad12679d5caa047861fa4504b994728ce84a909f01af88ae7c77cb5429482ec6028582792b12a45cbeb30de2ebe15f35d2999994ab396aacd878f1f9fbbf618b4059e824c5973469ff7075302b068bded03f6a6181afb4f874f3bef0fcb55304ad47f409db4f07066cd89ed75bb7d736253d2fc2c9a936692c1ab6141b56a7f3319c67a8344fa9a015b0914037edbbfbfe7086a336e6f47f08ee5c0116d2f67a7fb6eff17000000ffff03003a3b8e5ae2060000,2018-10-01 12:17:01+00,customer@example.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
3qtdXmycQwhWEPs91MExB2,\x1f8b0800000000000003c455cb769b4810dde72b7c5827b21002455e4502f2b0a48c1e96e538cea2a10bd106ba5177238bf8e8cb66319f34bf90a6c18aeca324e7cc6696dcba5575bb5efcfbf73f8fafcece0c828d8b33c3da487c9395e1ec215ef953d13727fe6ed8315e570c2119874f354dac97ae2b97c3b5b3783bbe4c7880a3c12a109793cdb07c3f8eb68bcdfd0767371aae7c5c3b338e81d7ce6ea122657f35401d3a07c0539692b0548cb6c63892a03e6cc7e9b4ecae69598e655b9d8eb611ba6524842b925514d3b6de5a7dc7b11d6d845d4e943361f499bd6737760c3913440e30e620847e8ef9b0bcf70a6bf6ddc3e3e48bdfe38e7f39ef4d2837931b424641af969973868b507ea211e399cea0bcabea559224641e88b08a37414202a70cc3d942a284d0b5f66f58aec2158b1669daa0795c0a12a254a1114a053cc15cbd51eb373bad76038605e740759d0c7f393714bad7e282a254053d254d5b3ea3ec45560dfb1922555e15b7ea0af077b043599e422b649971cc745941252f4fc4b825f90954bd5c9ecae812792a48d38eceaf4de609d33466f429c9a112aabd1e92e888ae7acec956976521abc15aebfaedf22270bc41b08ea362eeae9cd9865ba368e1dd93a49bf84bb21d24511c7ef83849ae7de806b71bdbba9e7deff5936ed767337be2e5e3cbd168301dd9dbeb6b67baeb2dcdb9d587190968107b62d52ffc9939c271dff5826c3d7ff335ef88f89b715a11013d8b8f77c6f0cabd332eeefe277977c6fe69df912cb4260a0f46b35a21e4b5e6c676a8708eca0ca8acb0afdfea0586a8a01805291ccf75034ffe34768a870987502ee7e38a184b998b8bf3f323deb98c114d44c98ada43eede83de97767d4122a5ed33932452bb5589aeb4495ed432e891e13729d456a6010a13d1d404f816f892a7c77cb54294826c61c8582b90a12a45cd6b315e6fbe11f23297ac99ca662d559b0f1fbfbf77f535a80b3c0119b3ea8e3eee0fa608605e7b9b07ec453134f69f2e9f76de1f962b6394a81f81ba69fee1ce3637b6db36cde6c6c6a4fa5b5437ad4906cf86056d55fbabc968ac1f91889f3360278162c02ffbf7738e60db8cdbcb11bc62297044ebd3d96abfdaff000000ffff0300fd61e9cae3060000,2018-10-01 12:24:16+00,customer@example.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
BtJGr7kt52QkZe8RdhKz42,\x1f8b0800000000000003c455c172da4810bde72b5c3a6f301208249f168462078cd780c115e21c469a161a4b9a9167465a888b2fcb219f945fc86824b3d8c566abf692a3de7b3dfda6a7bbf5e3dbf7e777676706c1c6c5993194e34bde4fa46dcd923538731c4fbe762de38f4a2124e3f051cb3a62b3f43cb91c6e7a0be77a9cf0004783fb408ca74fc3dd87eba85c3c3d5ef6b693e1bd8feb60c631f03ad82bd449d95f0d501f9d03e05b969270a7146d8d7124417dd83dc76a5996d9711dc771db3dcd115a3212c21dc92a8969779c8edb6ff72d4dc236272a9830fa9a771b1e43ce0491038c3908a1af73152d3ee55bfea9fb784978de21ebf286925537598ef12258737f52dbcc39c345283fd288f14c6750d155f52a4b12b21188b03a6f8a84044e1986b3854409a11b1ddfa83c852b152dd2b441f3782748885285462815f002737547eddf6cb79c7ebf6d9b96dd7061c139505d2ec35fce0d85eeb5c7a0d8a9ba9e72a8991b94bd49ae613f43a44a6fa4880b4168028efbe7a6025b21cb8c63a9c70a2af9eec4216b929f405505e4a9941e91a70e699ec5fa77ca3c41ddc68cbe243994423df308497424576fcf49a9ebb29055836d7401b77911f44683601347c5dcbbefcd9e7867122d468f24e926fe929483248ac3cbab69b2f2a11bac9fecce6af6b5ef26ddaecf66f674945f8f2793c1edc42e57abdeedb6bf34e71d176624a0413c12f76ee1cfcc098e5d6f14649bf9fbcfb925e22fc6694704744f3e3f18c33befc1b878f84df61e8cfdcbdc2359684f14fe369a110b21af3d37dca1c239da654065857dfe520f324405c52848e1b8bf1b78fa9f7da784987008e5727e5d2963297371717e0e5b94e52954ba7319239a881d2bea08b9fd007a70daf52a8994b91b2649a486ac725d9993bca87dd023e21729d478a6010a13d1140578097cc9d363bdf24d29c816868cb50219aa5ad4ba16e3f50a3042becb256bdab2194cf5ce878f5f2fbe7a2dd4159e828c59b5509ff7072a0298d7d1e6017b530c8dfdaf15a883f787e9ca1825ea8fa0969b7f58b8cdb2edb64dbb59b631a97e1bd5726b92c1ab6e41a57aeaaa351af60a89f8b502b6122806fcf6fdfe6924289b7e7bdb83772c058e68bd435bed77fb9f000000ffff030034f07389ec060000,2018-10-01 12:31:12+00,customer@gmail.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
VWB3riH9WuCv9NtSedaGE6,\x1f8b0800000000000003c455c1729b4810bde72b5c9cb3b21012029f222162c792bc9664a494631f0668c404984133032bc5a52fcb219f945fc83060457629deaabdec91f75e4fbfe9e96e7e7efff1f4eeec4cc3a17671a62d574383e12b7b5538a57d231610a24bd7d4de570a2e28834f4a66f0b5e738c21baecd8535b94e981f468395cfafa79be1eee3242a179baf97e6763c5cb9611d4c5908ac0e760a7952f67703d447e700e12d4d71b0938ab6c21812203f7aa6d569753aba615b9665b74dc56152521cc01dce2a89de332cc3eeebb6ae48d8e65806634a5ef056bbe143c829c76210860c3857d7e96fd6b6037d4f1457c10adb9fbd9bfb8531b059d17326cbcbd5106a9b39a36111884f24a22c5319647455bdca92806c043ca8ce9b222e80111ac2d942a00493b58a6f548ec4a58a1469daa079bce33840a944239472788699bca3f2afb75b56bfdfeee99d5ec30505634054b934d79b6b12dd2b8f7eb193753de5503137287b955cc16e8670955e4b11e31c93042cfbc3ba025b01cdb463a9430b22d8eec421f7383f81ca0a8853291d2c4e1dd23c4be7cf947e82ba8d29794e7228857ce61112e8482edf9ee152d56521aa065bab026ef3c23747037f1d47c5dc5999b30d33c6d162f41527ddc4f5703948a238b8bc9a264b17bafefda6672c67dffa76d2edba74d69b8ef2c9f5783cb81df7cae5d2bcddf63d7d6ed830c33ef1e3115fd9853bd3c7616c3b233f5bcffffa927778fca89d768441f5e4d38336bc731eb48b87ffc9de83b67f9e7b240ae589c03f5a336201e4b5e7863b543847bb0c88a8b02f8ff52043549010f9291cf777034fffb5efa430c40c02e1cd2795321622e717e7e7b045599e42a53b17312209dfd1a28e10db8fa006a75daf92489abba1024772c82ad79539c18ada073922de4821c733f55190f0a628c04a601e4b8ff5d23721205a2164b4e58b40d6a2d6b528ab578016b05d2e68d396cd60ca773e7cbcbdf8eab55057780a22a6d5427dda1fa808605e47eb07ec553114f69f56a00ade1fa62ba304cb3f825c6eee61e136cbb6dbd6cd66d9c6b8fa6d54cbad49062fba0595f2a9abd668d82bc4e3970ad80a202184afdfef772341d9f4dbeb1ebca3293044ea1dda6abfdbff020000ffff0300e1a26ae9ec060000,2018-10-01 12:33:11+00,customer@gmail.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
At4CDM5vfewV2WDHEPKRbt,\x1f8b0800000000000003c4554d73a33810bdcfaf4871de758c0db6c9696d20c9c471c61fb15d35933908688c06908824183329ffb23dec4fdabfb092205e27e59daddacb1e79efb5faa9d5ddfcf9fb1f2f1f2e2e0c1c195717c65858ae37b3ab18be6f7a5befd69f4f9781307e510a2e28838f5ad6e7bbb5eb8af56437588deeef521644f1781bf0bbd9f3a4bebe8fabd5f3b79bc17e3ad9fa51134c5904ac09764b7952fea9059aa30b80684e331cd652d1d5184302e4873d18f53b3db33f1c398edd1d3a9ac3a4a23884479c2b8969f74756d71c0d4c4dc2bec0321853f286ef0d5b3e8282722cc651c480737d9db1fde361b901320bf8c4ab83bb38c2dc22d73dd7de06f5eac6228dcd82d1a80cc547125396eb0c325a554f5912907bc04375de0c71018cd0082e5602a598ec747cab72252e55a4ccb2162d929ae31065128d51c6e11566f28ecabf351a76468ee90c7bc3960a4bc680e86a19fe7a6948f4a02d06652dcb7acea0661e50fe2eb786fd1c6195ddc810e31c931446ce6f3b0576429a1ba752979644b0facc219f717106950510e752ba589c3ba47d95de3f53e6196a9e50f29ae4580af9ca1e12e8442e9f9ee14ad76525547fed7401f745190cbc71b04be272e96e078b67d69fc62bef1b4eadd45fe36a9cc64978733b4b373e58c1e767bbbf59fc183aa965f97461cfbce2fe6e3a1dcfa776b5d90ce6fbe1da5cf61d58e0800489c7b74ee92fcc699438ae17e4bbe5af5f8a1e4fbe1ae71d61d02df9f2644c1edd27e3eae97fb2f7641c5ec71e89527b22f0dd68272c84a2f1dc72c70a17a8ce8108857df9dacc31c425895090c1697bb7f0ec5ffb4e0a23cc2014ebe5bd52264214fceaf212f6282f3250ba4b912092f29a964d84d85f839e9b6eb3496269ee810a1ccb1953ae9539c1cac60739217e92424e6716a030e56d518055c0d62c3bd54bdf8480e84490d34e2042598b46d7a1acd90046c8ea42d0b62ddbc194ef7cfcf8f9de6bb64253e1198884aa7dfa72385231c0b289368fd8bb6268ec3f6d401d7c384e574e09963f04b9dbfce3be3deeda41bfddb509567f0db5dbda64f0a65b50259f5ab546cbde229ebc55c05e0089207aff7e7f3712546dbfbdefc1479a0143a459a19dee87c35f000000ffff0300011a2ad8eb060000,2018-10-01 13:51:01+00,customer@gmail.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
|
11
BTCPayServer.Tests/TestData/OldPayments.csv
Normal file
11
BTCPayServer.Tests/TestData/OldPayments.csv
Normal file
@ -0,0 +1,11 @@
|
||||
Id,Blob,InvoiceDataId,Accounted,Blob2,Type
|
||||
a8be47ca93a1583f60b25fb1279b6c162af731024d169f8d1cd0d29d1d391132-0,\x1f8b0800000000000003748f396ec33010457b9f4260ed82c39d2ead20958b2050a966c425201289824205100c9f2c458e942b447216b8c9af66fe03de603edf3fcebbaa22537021bd05dfa43e904305921b4139a36cbfd13c9731a7a1ac847006c02d78eb3df3149cf13682129e320e513304a59ced98ee40c68e29caa33408963b14ba0b68e84fc89f79be7a9d1692de04345a10d26b0116c0482339a008144da7509a2e4aca95d7511923b534fadb87cee57928c1afca32cde15aba69194baeb3df5e23c7a62637f5032e7d18ca1d16dce8b925751e629a7a2c290ff5666bc901ecbe258fc7fb75bc6a5b720a4fe896758ff8f21a2eff289b65fc3d7a4acf81ec2e5f000000ffff03002eb110c572010000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
||||
c703dd8ae34622dfb96d677260631c39af483918d9e63676839be0db1b8a9268-1,\x1f8b0800000000000003748f3d6ec3300c85f79cc2d09c413fb644658c8b4e198ac2a3175aa20ba1b565b8720123c8c93af448bd42e5040dbaf44de47bc0f7c8efcfaff3ae28d84c8ec207f9260cc40e85a84a59951c14df6f695cd214c39872c2345809283adf11b71d28a39526edad00654bc05e59e1b2c58dd446fbcefa5eca529302f49e2b67b8e057b13b79b971bdecace37709835694d0a1e08012786572a3b2d944aba4ef2de81e205f50213907e6c643e7e23226f21999e685aea69bd729c53afaed35766c6af6c77ec275a0313d60c22d3db7ac8e631fe601538863bdd15a76a8a4d8b7ecf9f898e72bb765277a41b7e6bdc7b777bafcc36cd6e9b7f5145e89ed2e3f000000ffff03000f0bf2f373010000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
||||
f237a3d75018f4435927f5b1be1a8902045d88ddda39c25254214c3e484bd383-1,\x1f8b080000000000000374904d6ac3301046f73945d03a14fdc6a32ce3925528a564e9cd581a1593da0a8e9c62424ed6458fd42b547669e8a6b39bf7c193e6fbfaf8bc2e964b76a1fedcc48e6d967c35ed3d396a2ee40f4d4b190aa3a5e5609599d38ed27bec8f3b9a32fec0a791731287748a4d973267a0bcd2b50645dac96c30d249653d7a0f603cd75c821558532d829185b15a052dc0705fa05245905ccc62ceeee661f66ac36bedf97d44815668a851704009dc14be26653344aba40f16d601c0d660909c83e2c787cec5a14be4b332f503cdd0f5e329c532fae930b63d94ec0f7ec6b1a52e3d62c229bd56ac8c5d68fa1653aeae9c6c15db88756e6a55b197ed2e6fb3b9624ff7c232cb8565b4a75774635e03be9de9f6cf4387f1f4fb957d7324b6b87d030000ffff0300eef4b353b2010000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
||||
aedd36b5c976e56543cd7a5e207ea13dc801764cb3682474ab75a54399f20c27-9,\x1f8b08000000000000037490bf6ec2301087779e02794695ff3b66241513aaaa8a91e5629fab8812a3e050458827ebd047ea2bd44e0bead29b7cdfcffacebeaf8fcfcb6c3e2767ec4f6decc872ce16a5efd1617b46bf6d0f58a052dc68658d9cd20ed37becf76b2c197da0a5f894c4211d63dba5cc0937d4056ead14a08c82c6482e75d508e98ca6cc55c20333c8a94203ce4ba1152aa39d6d94d0de03523b8929b99b87c99b6779c128a3bfc50c582621d0c02b69a571ba623a7068b4e516adb0547b45196f022aea2bf3e303e7e2d025f45999fa0127e8faf198621d7df918596d6bf2073fc378c02e3d4282925e76a48e5d68fb03a4bcbabad87664c9163bf2b25ae75380b713e6eee9bead0cf3b632dae02bb8f176e7facf94ed78bcbd63d3ee91ccaedf000000ffff030012c428b2af010000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
||||
3358197dcb30420d3487516e8ec9dcc0f6d03f16b4f41b77cd3ad429b1e529ee-0,\x1f8b0800000000000003748fb14ec3301086f73e45e4b9839db3e3b86383983a209431cbe57c4116248e828314557d32061e8957202914b170d3ddff49dfe9ff7cff38efb24c4c4c1cded8d7a16771c894d18536e024ec371ae734c630a49508e6dcb16955eebc06246fad6a3bdd6a5540e7655790f444aee4828d2a2d68e9750e925aeb95332580fc19f16b9eaf5e50680dc8fcc69545a774993b7285c1429adc90266d491a8980502290b2d2b80ebd85d27efb9028ce4362bf2ad334f335a4691953aca2dfaa89635d893ff1032e3d0fe90e136ef4dc882a0e5d987a4c210ed5666bc441ed1bf178bc5fb70e5f5e79bd4efc84b4dc82cb3fca7a196f4f4fe199c5eef2050000ffff0300b173d7d272010000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
||||
03d66d2d633dac96cc45546a0555cb917f5d2f0d37dddfdf67e48f88ae728aa1-5,\x1f8b08000000000000037490bd6ec2301446779e027946951dff33928a095555c598e5625fa38812a3e050458827ebd047ea2bd40e2aead2ebe99e4f3ab6bfefcfafeb6c3e2717eccf6decc872ce1665efd1617b41bf6d8f58a0144652a3f93ded307dc4feb0c692d1275aa69a9238a4536cbb94390166405780c6988042691ff2f15c535f05e975b0cceda4a4528114423a671538ee15afbcf25e514ee524a6e4611e26af50a085a68f611a2c1312ad1396a3715c280f2cd8c0b50dc2535571b6432a0ce74aed8cbefbc0b93874097d56a67ec009ba7e3ca558475f3e4656db9afcc1af301eb14bcf90a0a4d786d4b10b6d7f8494abab8bad214bc9b45a34e46db5cecb246ec8cba3afcc725f196d700f6ecc6b80f733defeb9673b9e7e5fb2690f4866b71f000000ffff03007bb33a2fb1010000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
||||
dc130d025a4bd2e7ee83b7707d850e7c1a52872c222a5bc2e05d3357e79aa762-1,\x1f8b08000000000000037490bf6ec2301087779e027946e8fc2f761849c584aaaa6264b9d817145162141caa08f1641dfa487d85daa98abaf426dfefb3beb3efebe3f3369bcfd995fa4b1b3ab69af345ee7b72d45ec9efda13e5506bae1557d64eb4a3f81efae386328325e4121309433c87b68b296785405322196da4d49ec0095d0b1442386bb4e0681c90d5c61ba88d954464bc50b54610e0b9f40ef82406f6300f9317ea74c5c1a3b8c1922b4da553a524eba42a3cf2a66ca4291be5a11092d704ca4a5914b5353f3e742e0c5d249f940dbe5d684a5d3f9e63a882cf3f63eb5dc5fec42f389ea88b4f1831d3db9e55a16bdafe8431edaecaba3d5bc162cf5ed79b748afd40a9797e6c2bd365e65b3aa01b533b4dbeff3364379e7f9fb16d8fc466f76f000000ffff0300f93b039faf010000,Q7RqoHLngK9svM4MgRyi9y,f,,
|
||||
afc39884e024cbb3a48ca997b7e878b199d85fe97f90f94471364cc866ba83ad-1,\x1f8b080000000000000374904d6ac3301085f73945d03a14c992f593655cb20aa5942cbd1949a362d258c191534cc8c9bae8917a854a2e0ddd7476ef3de6d3e87d7d7c5e17cb25b9e070ee624fd64bb62a7a4087dd05fdbe3b6231eb9ad5a21246ce698fe93d0e872d968c3ed032d59cc4319d62d7a7ec13f09a5b90d269e1b8544c88600c55014d1dbc36c632a5515b651418ed4058ee6c25906a91258740d90ca6e44e1e672eb5402b47efc3f23e13351a270cc7bc2aa407164ce0ca04e1a9ac38b34885e65c4aabd50f0f9c8b639fd067641a469c4d374ca7149be8cbc7c866df903ff6334c47ecd3232428e9b5254dec43371c21e5ea9a426bc95ae59e562d79d96cb398c12d79baf795bddc57b676f80a6eca32c0db196fffbcb39f4ebf97ecba0392c5ed1b0000ffff030018cc54a1b1010000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
||||
6f2b8513ebfdb14b41d4de235f050cd028400cb01fde700d72c6bedc2ad8af1e-5,\x1f8b08000000000000037490bf6ac3301087f73c45d01cc249966425635c3285528a472ffa732a26b5151c39c5843c59873e525fa192434397de22ddef83efa4fbfefcba2e964b72c1e1dc869e6c977495fb012db6177475db610e85908a6d24c04c7b8c1f6138ee313358432e369330c65368fb987242517ba79876d6a0952503578243ea0d80e5c0944b2708e159e1d0714eb931d43b34b4508219e941cc62200ff378f752c719c2a368a937942b445158855e6967b8b05c0a2e9934c0b59585960c0b55d272a3cabb4f5b1bc63ea24b4aafdfcf38a776984e3154c1e59f915d5d913ff18b9e3aece3938e3ad36b43aad0fb76e8744cbbabb2ae215b5835e475b74fb7388c989ae7c7b6325d677ec0376da7d4ce936fff0ca9a7d3ef330eed11c9e2f6030000ffff0300b09cf40daf010000,Q7RqoHLngK9svM4MgRyi9y,f,,
|
||||
3a6659e189f83f649c0010305def1b8fb78efdbe8c0ce44d56c8c22fff742b55-61,\x1f8b08000000000000037490bd6ec3201485f73c45c41c5580f973c6b8ca14555595d10be65e222b8d891c9cca8af2641dfa487d8582ab585d7a27ce39f001e7fbf3ebb6582ec915fb4b1b3ab25eb255d63d3a6caf08fbf684d994525121a85653da61fc08fd718b39a34f340f9f9230c47368bb987c22256fb4f09e7bc79d914a000aea8c6bd083c1461bcfd2524241593a5f3a250aef8d2991c952a9c21630812999c9c3c4d55615cad37998b625131eb8e5c080496c941525958c97b2708c838286022ac5b56060f42fcf3a17862e222464ec079c4cd78fe718aa00f96364b3afc81ffbd58e27ece2b38d36a7b79a54a1f36d7fb2315557655a4dd64c30b1aac9db669b84b7ef174cea652e2c99a9b064edf060ddf8d873ffe7a2fd787e3c65d71e912cee3f000000ffff0300a991d8c4b2010000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
|
@ -464,7 +464,7 @@ retry:
|
||||
|
||||
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();
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-sortable", "sortable.min.js").Trim();
|
||||
@ -494,6 +494,10 @@ retry:
|
||||
version = Regex.Match(actual, "Original file: /npm/decimal\\.js@([0-9]+.[0-9]+.[0-9]+)/decimal\\.js").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/decimal.js@{version}/decimal.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bbqr", "bbqr.iife.js").Trim();
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/bbqr@1.0.0/dist/bbqr.iife.js")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
}
|
||||
|
||||
private void EqualJsContent(string expected, string actual)
|
||||
|
@ -1480,6 +1480,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanSetPaymentMethodLimits()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
@ -1515,9 +1516,10 @@ namespace BTCPayServer.Tests
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
Assert.Single(invoice.CryptoInfo);
|
||||
Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType);
|
||||
// LN and LNURL
|
||||
Assert.Equal(2, invoice.CryptoInfo.Length);
|
||||
Assert.Contains(invoice.CryptoInfo, c => c.PaymentType == PaymentTypes.LNURLPay.ToString());
|
||||
Assert.Contains(invoice.CryptoInfo, c => c.PaymentType == PaymentTypes.LightningLike.ToString());
|
||||
|
||||
// Let's replicate https://github.com/btcpayserver/btcpayserver/issues/2963
|
||||
// We allow BTC for more than 5 USD, and LN for less than 150. The default is LN, so the default
|
||||
@ -2272,18 +2274,18 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
|
||||
// Test on the webhooks
|
||||
user.AssertHasWebhookEvent<WebhookInvoiceSettledEvent>(WebhookEventType.InvoiceSettled,
|
||||
await user.AssertHasWebhookEvent<WebhookInvoiceSettledEvent>(WebhookEventType.InvoiceSettled,
|
||||
c =>
|
||||
{
|
||||
Assert.False(c.ManuallyMarked);
|
||||
Assert.True(c.OverPaid);
|
||||
});
|
||||
user.AssertHasWebhookEvent<WebhookInvoiceProcessingEvent>(WebhookEventType.InvoiceProcessing,
|
||||
await user.AssertHasWebhookEvent<WebhookInvoiceProcessingEvent>(WebhookEventType.InvoiceProcessing,
|
||||
c =>
|
||||
{
|
||||
Assert.True(c.OverPaid);
|
||||
});
|
||||
user.AssertHasWebhookEvent<WebhookInvoiceReceivedPaymentEvent>(WebhookEventType.InvoiceReceivedPayment,
|
||||
await user.AssertHasWebhookEvent<WebhookInvoiceReceivedPaymentEvent>(WebhookEventType.InvoiceReceivedPayment,
|
||||
c =>
|
||||
{
|
||||
Assert.False(c.AfterExpiration);
|
||||
@ -2293,7 +2295,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.StartsWith(txId.ToString(), c.Payment.Id);
|
||||
|
||||
});
|
||||
user.AssertHasWebhookEvent<WebhookInvoicePaymentSettledEvent>(WebhookEventType.InvoicePaymentSettled,
|
||||
await user.AssertHasWebhookEvent<WebhookInvoicePaymentSettledEvent>(WebhookEventType.InvoicePaymentSettled,
|
||||
c =>
|
||||
{
|
||||
Assert.False(c.AfterExpiration);
|
||||
@ -2780,7 +2782,7 @@ namespace BTCPayServer.Tests
|
||||
await tester.StartAsync();
|
||||
|
||||
var acc = tester.NewAccount();
|
||||
acc.GrantAccess(true);
|
||||
await acc.GrantAccessAsync(true);
|
||||
|
||||
var settings = tester.PayTester.GetService<SettingsRepository>();
|
||||
var emailSenderFactory = tester.PayTester.GetService<EmailSenderFactory>();
|
||||
@ -2805,14 +2807,14 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("admin@admin.com", (await Assert.IsType<ServerEmailSender>(await emailSenderFactory.GetEmailSender()).GetEmailSettings()).Login);
|
||||
Assert.Null(await Assert.IsType<StoreEmailSender>(await emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings());
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(await acc.GetController<UIStoresController>().StoreEmailSettings(acc.StoreId, new EmailsViewModel(new EmailSettings()
|
||||
Assert.IsType<RedirectToActionResult>(await acc.GetController<UIStoresController>().StoreEmailSettings(acc.StoreId, new EmailsViewModel(new EmailSettings
|
||||
{
|
||||
From = "store@store.com",
|
||||
Login = "store@store.com",
|
||||
Password = "store@store.com",
|
||||
Port = 1234,
|
||||
Server = "store.com"
|
||||
}), ""));
|
||||
}), "", true));
|
||||
|
||||
Assert.Equal("store@store.com", (await Assert.IsType<StoreEmailSender>(await emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings()).Login);
|
||||
}
|
||||
@ -2986,7 +2988,7 @@ namespace BTCPayServer.Tests
|
||||
[Trait("Selenium", "Selenium")]
|
||||
public async Task CanCreateReports()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
using var tester = CreateServerTester(newDb: true);
|
||||
tester.ActivateLightning();
|
||||
tester.DeleteStore = false;
|
||||
await tester.StartAsync();
|
||||
@ -3084,6 +3086,13 @@ namespace BTCPayServer.Tests
|
||||
.ToDictionary(d => d.Key, r => r.Sum(d => d[countIndex].Value<int>()));
|
||||
Assert.Equal(8, itemsCount["green-tea"]);
|
||||
Assert.Equal(1, itemsCount["black-tea"]);
|
||||
|
||||
await acc.ImportOldInvoices();
|
||||
var date2018 = new DateTimeOffset(2018, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
report = await GetReport(acc, new() { ViewName = "Payments", TimePeriod = new TimePeriod() { From = date2018, To = date2018 + TimeSpan.FromDays(365) } });
|
||||
var invoiceIdIndex = report.GetIndex("InvoiceId");
|
||||
var oldPaymentsCount = report.Data.Count(d => d[invoiceIdIndex].Value<string>() == "Q7RqoHLngK9svM4MgRyi9y");
|
||||
Assert.Equal(8, oldPaymentsCount); // 10 payments, but 2 unaccounted
|
||||
}
|
||||
|
||||
private async Task<StoreReportResponse> GetReport(TestAccount acc, StoreReportRequest req)
|
||||
|
@ -163,7 +163,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
image: btcpayserver/lightning:v24.02.2
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -171,6 +171,7 @@ services:
|
||||
LIGHTNINGD_CHAIN: "btc"
|
||||
LIGHTNINGD_NETWORK: "regtest"
|
||||
LIGHTNINGD_OPT: |
|
||||
developer
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
announce-addr=customer_lightningd:9735
|
||||
@ -190,13 +191,14 @@ services:
|
||||
- bitcoind
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
image: btcpayserver/lightning:v24.02.2
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_CHAIN: "btc"
|
||||
LIGHTNINGD_NETWORK: "regtest"
|
||||
LIGHTNINGD_OPT: |
|
||||
developer
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
announce-addr=merchant_lightningd:9735
|
||||
|
@ -149,7 +149,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
image: btcpayserver/lightning:v24.02.2
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -157,6 +157,7 @@ services:
|
||||
LIGHTNINGD_CHAIN: "btc"
|
||||
LIGHTNINGD_NETWORK: "regtest"
|
||||
LIGHTNINGD_OPT: |
|
||||
developer
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
announce-addr=customer_lightningd:9735
|
||||
@ -176,13 +177,14 @@ services:
|
||||
- bitcoind
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
image: btcpayserver/lightning:v24.02.2
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_CHAIN: "btc"
|
||||
LIGHTNINGD_NETWORK: "regtest"
|
||||
LIGHTNINGD_OPT: |
|
||||
developer
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
announce-addr=merchant_lightningd:9735
|
||||
|
@ -50,7 +50,7 @@
|
||||
<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.5.4" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.6.0" />
|
||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||
<PackageReference Include="Dapper" Version="2.1.28" />
|
||||
<PackageReference Include="Fido2" Version="2.0.2" />
|
||||
|
@ -26,16 +26,16 @@
|
||||
{
|
||||
@if (Model.Store != null)
|
||||
{
|
||||
<div class="accordion-item" permission="@Policies.CanModifyStoreSettings">
|
||||
<div class="accordion-item" permission="@Policies.CanViewStoreSettings">
|
||||
<div class="accordion-body">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
|
||||
<a asp-area="" asp-controller="UIStores" asp-action="Dashboard" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.Dashboard)" id="StoreNav-Dashboard">
|
||||
<vc:icon symbol="home"/>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" permission="@Policies.CanViewStoreSettings">
|
||||
<a asp-area="" asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(new [] {StoreNavPages.Rates, StoreNavPages.CheckoutAppearance, StoreNavPages.General, StoreNavPages.Tokens, StoreNavPages.Users, StoreNavPages.Webhooks, StoreNavPages.PayoutProcessors, StoreNavPages.Emails})" id="StoreNav-StoreSettings">
|
||||
<vc:icon symbol="settings"/>
|
||||
<span>Settings</span>
|
||||
@ -115,7 +115,6 @@
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -297,6 +296,15 @@
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.ContactUrl))
|
||||
{
|
||||
<li class="nav-item">
|
||||
<a href="@Model.ContactUrl" class="nav-link" id="Nav-ContactUs">
|
||||
<vc:icon symbol="contact"/>
|
||||
<span>Contact Us</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</nav>
|
||||
|
@ -29,6 +29,8 @@ namespace BTCPayServer.Components.MainNav
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
|
||||
private readonly CustodianAccountRepository _custodianAccountRepository;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
public PoliciesSettings PoliciesSettings { get; }
|
||||
|
||||
public MainNav(
|
||||
AppService appService,
|
||||
@ -38,6 +40,7 @@ namespace BTCPayServer.Components.MainNav
|
||||
UserManager<ApplicationUser> userManager,
|
||||
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
|
||||
CustodianAccountRepository custodianAccountRepository,
|
||||
SettingsRepository settingsRepository,
|
||||
PoliciesSettings policiesSettings)
|
||||
{
|
||||
_storeRepo = storeRepo;
|
||||
@ -47,13 +50,19 @@ namespace BTCPayServer.Components.MainNav
|
||||
_storesController = storesController;
|
||||
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
|
||||
_custodianAccountRepository = custodianAccountRepository;
|
||||
_settingsRepository = settingsRepository;
|
||||
PoliciesSettings = policiesSettings;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync()
|
||||
{
|
||||
var store = ViewContext.HttpContext.GetStoreData();
|
||||
var vm = new MainNavViewModel { Store = store };
|
||||
var serverSettings = await _settingsRepository.GetSettingAsync<ServerSettings>() ?? new ServerSettings();
|
||||
var vm = new MainNavViewModel
|
||||
{
|
||||
Store = store,
|
||||
ContactUrl = serverSettings.ContactUrl
|
||||
};
|
||||
#if ALTCOINS
|
||||
vm.AltcoinsBuild = true;
|
||||
#endif
|
||||
@ -92,7 +101,5 @@ namespace BTCPayServer.Components.MainNav
|
||||
}
|
||||
|
||||
private string UserId => _userManager.GetUserId(HttpContext.User);
|
||||
|
||||
public PoliciesSettings PoliciesSettings { get; }
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ namespace BTCPayServer.Components.MainNav
|
||||
public CustodianAccountData[] CustodianAccounts { get; set; }
|
||||
public bool AltcoinsBuild { get; set; }
|
||||
public int ArchivedAppsCount { get; set; }
|
||||
public string ContactUrl { get; set; }
|
||||
}
|
||||
|
||||
public class StoreApp
|
||||
|
@ -238,7 +238,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
return new CrowdfundSettings
|
||||
{
|
||||
Title = request.Title?.Trim(),
|
||||
Title = request.Title?.Trim() ?? request.AppName,
|
||||
Enabled = request.Enabled ?? true,
|
||||
EnforceTargetAmount = request.EnforceTargetAmount ?? false,
|
||||
StartDate = request.StartDate?.UtcDateTime,
|
||||
@ -272,8 +272,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
return new PointOfSaleSettings
|
||||
{
|
||||
Title = request.Title,
|
||||
Title = request.Title ?? request.AppName,
|
||||
DefaultView = (PosViewType)request.DefaultView,
|
||||
ShowItems = request.ShowItems,
|
||||
ShowCustomAmount = request.ShowCustomAmount,
|
||||
ShowDiscount = request.ShowDiscount,
|
||||
ShowSearch = request.ShowSearch,
|
||||
@ -335,6 +336,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Created = appData.Created,
|
||||
Title = settings.Title,
|
||||
DefaultView = settings.DefaultView.ToString(),
|
||||
ShowItems = settings.ShowItems,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
ShowDiscount = settings.ShowDiscount,
|
||||
ShowSearch = settings.ShowSearch,
|
||||
|
@ -326,7 +326,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
if (result == null)
|
||||
{
|
||||
return this.CreateAPIError(404, "trade-not-found",
|
||||
$"Could not find the the trade with ID {tradeId} on {custodianAccount.Name}");
|
||||
$"Could not find the trade with ID {tradeId} on {custodianAccount.Name}");
|
||||
}
|
||||
return Ok(ToModel(result, accountId, custodianAccount.CustodianCode));
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO.IsolatedStorage;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
@ -12,6 +14,7 @@ using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Security;
|
||||
@ -22,8 +25,11 @@ using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Org.BouncyCastle.Bcpg.OpenPgp;
|
||||
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
|
||||
|
||||
namespace BTCPayServer.Controllers.Greenfield
|
||||
@ -43,6 +49,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly BTCPayServerEnvironment _env;
|
||||
private readonly Logs _logs;
|
||||
|
||||
public GreenfieldPullPaymentController(PullPaymentHostedService pullPaymentService,
|
||||
LinkGenerator linkGenerator,
|
||||
@ -53,7 +60,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
IAuthorizationService authorizationService,
|
||||
SettingsRepository settingsRepository,
|
||||
BTCPayServerEnvironment env)
|
||||
BTCPayServerEnvironment env, Logs logs)
|
||||
{
|
||||
_pullPaymentService = pullPaymentService;
|
||||
_linkGenerator = linkGenerator;
|
||||
@ -65,6 +72,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_authorizationService = authorizationService;
|
||||
_settingsRepository = settingsRepository;
|
||||
_env = env;
|
||||
_logs = logs;
|
||||
}
|
||||
|
||||
[HttpGet("~/api/v1/stores/{storeId}/pull-payments")]
|
||||
@ -200,13 +208,39 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[HttpPost]
|
||||
[Route("~/api/v1/pull-payments/{pullPaymentId}/boltcards")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> RegisterBoltcard(string pullPaymentId, RegisterBoltcardRequest request)
|
||||
public async Task<IActionResult> RegisterBoltcard(string pullPaymentId, RegisterBoltcardRequest request, string? onExisting = null)
|
||||
{
|
||||
if (pullPaymentId is null)
|
||||
return PullPaymentNotFound();
|
||||
this._logs.PayServer.LogInformation($"RegisterBoltcard: onExisting queryParam: {onExisting}");
|
||||
this._logs.PayServer.LogInformation($"{JsonConvert.SerializeObject(request)}");
|
||||
var pp = await _pullPaymentService.GetPullPayment(pullPaymentId, false);
|
||||
if (pp is null)
|
||||
return PullPaymentNotFound();
|
||||
var issuerKey = await _settingsRepository.GetIssuerKey(_env);
|
||||
|
||||
// LNURLW is used by deeplinks
|
||||
if (request?.LNURLW is not null)
|
||||
{
|
||||
if (request.UID is not null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.LNURLW), "You should pass either LNURLW or UID but not both");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
var p = ExtractP(request.LNURLW);
|
||||
if (p is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.LNURLW), "The LNURLW should contains a 'p=' parameter");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
if (issuerKey.TryDecrypt(p) is not BoltcardPICCData picc)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.LNURLW), "The LNURLW 'p=' parameter cannot be decrypted");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
request.UID = picc.Uid;
|
||||
}
|
||||
|
||||
if (request?.UID is null || request.UID.Length != 7)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.UID), "The UID is required and should be 7 bytes");
|
||||
@ -217,15 +251,28 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return this.CreateAPIError(400, "lnurl-not-supported", "This pull payment currency should be BTC or SATS and accept lightning");
|
||||
}
|
||||
|
||||
var issuerKey = await _settingsRepository.GetIssuerKey(_env);
|
||||
// Passing onExisting as a query parameter is used by deeplink
|
||||
request.OnExisting = onExisting switch
|
||||
{
|
||||
nameof(OnExistingBehavior.UpdateVersion) => OnExistingBehavior.UpdateVersion,
|
||||
nameof(OnExistingBehavior.KeepVersion) => OnExistingBehavior.KeepVersion,
|
||||
_ => request.OnExisting
|
||||
};
|
||||
|
||||
this._logs.PayServer.LogInformation($"After");
|
||||
this._logs.PayServer.LogInformation($"{JsonConvert.SerializeObject(request)}");
|
||||
|
||||
var version = await _dbContextFactory.LinkBoltcardToPullPayment(pullPaymentId, issuerKey, request.UID, request.OnExisting);
|
||||
this._logs.PayServer.LogInformation($"Version: " + version);
|
||||
this._logs.PayServer.LogInformation($"ID: " + Encoders.Hex.EncodeData(issuerKey.GetId(request.UID)));
|
||||
|
||||
var keys = issuerKey.CreatePullPaymentCardKey(request.UID, version, pullPaymentId).DeriveBoltcardKeys(issuerKey);
|
||||
|
||||
var boltcardUrl = Url.Action(nameof(UIBoltcardController.GetWithdrawRequest), "UIBoltcard");
|
||||
boltcardUrl = Request.GetAbsoluteUri(boltcardUrl);
|
||||
boltcardUrl = Regex.Replace(boltcardUrl, "^https?://", "lnurlw://");
|
||||
|
||||
return Ok(new RegisterBoltcardResponse()
|
||||
var resp = new RegisterBoltcardResponse()
|
||||
{
|
||||
LNURLW = boltcardUrl,
|
||||
Version = version,
|
||||
@ -234,7 +281,25 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
K2 = Encoders.Hex.EncodeData(keys.AuthenticationKey.ToBytes()).ToUpperInvariant(),
|
||||
K3 = Encoders.Hex.EncodeData(keys.K3.ToBytes()).ToUpperInvariant(),
|
||||
K4 = Encoders.Hex.EncodeData(keys.K4.ToBytes()).ToUpperInvariant(),
|
||||
});
|
||||
};
|
||||
this._logs.PayServer.LogInformation($"Response");
|
||||
this._logs.PayServer.LogInformation($"{JsonConvert.SerializeObject(resp)}");
|
||||
|
||||
return Ok(resp);
|
||||
}
|
||||
|
||||
private string? ExtractP(string? url)
|
||||
{
|
||||
if (url is null || !Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||
return null;
|
||||
int num = uri.AbsoluteUri.IndexOf('?');
|
||||
if (num == -1)
|
||||
return null;
|
||||
string input = uri.AbsoluteUri.Substring(num);
|
||||
Match match = Regex.Match(input, "p=([a-f0-9A-F]{32})");
|
||||
if (!match.Success)
|
||||
return null;
|
||||
return match.Groups[1].Value;
|
||||
}
|
||||
|
||||
[HttpGet("~/api/v1/pull-payments/{pullPaymentId}")]
|
||||
|
@ -24,7 +24,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_storeRepository = storeRepository;
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/roles")]
|
||||
public async Task<IActionResult> GetStoreRoles(string storeId)
|
||||
{
|
||||
@ -34,7 +34,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
: 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();
|
||||
|
@ -27,14 +27,15 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_storeRepository = storeRepository;
|
||||
_userManager = userManager;
|
||||
}
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/users")]
|
||||
public IActionResult GetStoreUsers()
|
||||
{
|
||||
|
||||
var store = HttpContext.GetStoreData();
|
||||
return store == null ? StoreNotFound() : Ok(FromModel(store));
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/users/{idOrEmail}")]
|
||||
public async Task<IActionResult> RemoveStoreUser(string storeId, string idOrEmail)
|
||||
|
@ -185,7 +185,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
RequiresEmailConfirmation = policies.RequiresConfirmedEmail,
|
||||
RequiresApproval = policies.RequiresUserApproval,
|
||||
Created = DateTimeOffset.UtcNow,
|
||||
Approved = !anyAdmin && isAdmin // auto-approve first admin
|
||||
Approved = isAdmin // auto-approve first admin and users created by an admin
|
||||
};
|
||||
var passwordValidation = await this._passwordValidator.ValidateAsync(_userManager, user, request.Password);
|
||||
if (!passwordValidation.Succeeded)
|
||||
@ -214,7 +214,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
if (request.IsAdministrator is true)
|
||||
var isNewAdmin = request.IsAdministrator is true;
|
||||
if (isNewAdmin)
|
||||
{
|
||||
if (!anyAdmin)
|
||||
{
|
||||
@ -233,7 +234,21 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
await _settingsRepository.FirstAdminRegistered(policies, _options.UpdateUrl != null, _options.DisableRegistration, Logs);
|
||||
}
|
||||
}
|
||||
_eventAggregator.Publish(new UserRegisteredEvent() { RequestUri = Request.GetAbsoluteRootUri(), User = user, Admin = request.IsAdministrator is true });
|
||||
|
||||
var currentUser = await _userManager.GetUserAsync(User);
|
||||
var userEvent = new UserRegisteredEvent
|
||||
{
|
||||
RequestUri = Request.GetAbsoluteRootUri(),
|
||||
Admin = isNewAdmin,
|
||||
User = user
|
||||
};
|
||||
if (currentUser is not null)
|
||||
{
|
||||
userEvent.Kind = UserRegisteredEventKind.Invite;
|
||||
userEvent.InvitedByUser = currentUser;
|
||||
};
|
||||
_eventAggregator.Publish(userEvent);
|
||||
|
||||
var model = await FromModel(user);
|
||||
return CreatedAtAction(string.Empty, model);
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ namespace BTCPayServer.Controllers
|
||||
return await Login(returnUrl);
|
||||
}
|
||||
|
||||
_logger.LogInformation("User with ID {UserId} logged in with a login code", user!.Id);
|
||||
_logger.LogInformation("User {Email} logged in with a login code", user!.Email);
|
||||
await _signInManager.SignInAsync(user, false, "LoginCode");
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
@ -215,7 +215,7 @@ namespace BTCPayServer.Controllers
|
||||
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User {UserId} logged in", user.Id);
|
||||
_logger.LogInformation("User {Email} logged in", user.Email);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
if (result.RequiresTwoFactor)
|
||||
@ -230,7 +230,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User {UserId} account locked out", user.Id);
|
||||
_logger.LogWarning("User {Email} tried to log in, but is locked out", user.Email);
|
||||
return RedirectToAction(nameof(Lockout), new { user.LockoutEnd });
|
||||
}
|
||||
|
||||
@ -368,7 +368,7 @@ namespace BTCPayServer.Controllers
|
||||
if (await _fido2Service.CompleteLogin(viewModel.UserId, JObject.Parse(viewModel.Response).ToObject<AuthenticatorAssertionRawResponse>()))
|
||||
{
|
||||
await _signInManager.SignInAsync(user!, viewModel.RememberMe, "FIDO2");
|
||||
_logger.LogInformation("User logged in");
|
||||
_logger.LogInformation("User {Email} logged in with FIDO2", user.Email);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
}
|
||||
@ -455,11 +455,11 @@ namespace BTCPayServer.Controllers
|
||||
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User with ID {UserId} logged in with 2fa", user.Id);
|
||||
_logger.LogInformation("User {Email} logged in with 2FA", user.Email);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
|
||||
_logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}", user.Id);
|
||||
_logger.LogWarning("User {Email} entered invalid authenticator code", user.Email);
|
||||
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel
|
||||
{
|
||||
@ -524,17 +524,17 @@ namespace BTCPayServer.Controllers
|
||||
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User with ID {UserId} logged in with a recovery code", user.Id);
|
||||
_logger.LogInformation("User {Email} logged in with a recovery code", user.Email);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User with ID {UserId} account locked out", user.Id);
|
||||
_logger.LogWarning("User {Email} account locked out", user.Email);
|
||||
|
||||
return RedirectToAction(nameof(Lockout), new { user.LockoutEnd });
|
||||
}
|
||||
|
||||
_logger.LogWarning("Invalid recovery code entered for user with ID {UserId}", user.Id);
|
||||
_logger.LogWarning("User {Email} entered invalid recovery code", user.Email);
|
||||
ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
|
||||
return View();
|
||||
}
|
||||
@ -600,7 +600,6 @@ namespace BTCPayServer.Controllers
|
||||
var settings = await _SettingsRepository.GetSettingAsync<ThemeSettings>() ?? new ThemeSettings();
|
||||
settings.FirstRun = false;
|
||||
await _SettingsRepository.UpdateSetting(settings);
|
||||
|
||||
await _SettingsRepository.FirstAdminRegistered(policies, _Options.UpdateUrl != null, _Options.DisableRegistration, Logs);
|
||||
RegisteredAdmin = true;
|
||||
}
|
||||
@ -614,15 +613,17 @@ namespace BTCPayServer.Controllers
|
||||
RegisteredUserId = user.Id;
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Account created.";
|
||||
if (policies.RequiresConfirmedEmail)
|
||||
var requiresConfirmedEmail = policies.RequiresConfirmedEmail && !user.EmailConfirmed;
|
||||
var requiresUserApproval = policies.RequiresUserApproval && !user.Approved;
|
||||
if (requiresConfirmedEmail)
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] += " Please confirm your email.";
|
||||
}
|
||||
if (policies.RequiresUserApproval)
|
||||
if (requiresUserApproval)
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] += " The new account requires approval by an admin before you can log in.";
|
||||
}
|
||||
if (policies.RequiresConfirmedEmail || policies.RequiresUserApproval)
|
||||
if (requiresConfirmedEmail || requiresUserApproval)
|
||||
{
|
||||
return RedirectToAction(nameof(Login));
|
||||
}
|
||||
@ -649,9 +650,11 @@ namespace BTCPayServer.Controllers
|
||||
[HttpGet("/logout")]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
var userId = _signInManager.UserManager.GetUserId(HttpContext.User);
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
await _signInManager.SignOutAsync();
|
||||
HttpContext.DeleteUserPrefsCookie();
|
||||
_logger.LogInformation("User logged out");
|
||||
_logger.LogInformation("User {Email} logged out", user!.Email);
|
||||
return RedirectToAction(nameof(Login));
|
||||
}
|
||||
|
||||
@ -670,25 +673,31 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
var result = await _userManager.ConfirmEmailAsync(user, code);
|
||||
if (!await _userManager.HasPasswordAsync(user))
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_eventAggregator.Publish(new UserConfirmedEmailEvent
|
||||
{
|
||||
User = user,
|
||||
RequestUri = Request.GetAbsoluteRootUri()
|
||||
});
|
||||
|
||||
var hasPassword = await _userManager.HasPasswordAsync(user);
|
||||
if (hasPassword)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "Your email has been confirmed."
|
||||
});
|
||||
return RedirectToAction(nameof(Login), new { email = user.Email });
|
||||
}
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Message = "Your email has been confirmed but you still need to set your password."
|
||||
Message = "Your email has been confirmed. Please set your password."
|
||||
});
|
||||
return RedirectToAction("SetPassword", new { email = user.Email, code = await _userManager.GeneratePasswordResetTokenAsync(user) });
|
||||
}
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "Your email has been confirmed."
|
||||
});
|
||||
return RedirectToAction("Login", new { email = user.Email });
|
||||
return await RedirectToSetPassword(user);
|
||||
}
|
||||
|
||||
return View("Error");
|
||||
@ -740,17 +749,23 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (code == null)
|
||||
{
|
||||
throw new ApplicationException("A code must be supplied for password reset.");
|
||||
throw new ApplicationException("A code must be supplied for this action.");
|
||||
}
|
||||
|
||||
var user = string.IsNullOrEmpty(userId) ? null : await _userManager.FindByIdAsync(userId);
|
||||
var hasPassword = user != null && await _userManager.HasPasswordAsync(user);
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
{
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
email = user?.Email;
|
||||
}
|
||||
|
||||
var model = new SetPasswordViewModel { Code = code, Email = email, EmailSetInternally = !string.IsNullOrEmpty(email) };
|
||||
return View(model);
|
||||
return View(new SetPasswordViewModel
|
||||
{
|
||||
Code = code,
|
||||
Email = email,
|
||||
EmailSetInternally = !string.IsNullOrEmpty(email),
|
||||
HasPassword = hasPassword
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("/login/set-password")]
|
||||
@ -762,7 +777,9 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||
var hasPassword = user != null && await _userManager.HasPasswordAsync(user);
|
||||
if (!UserService.TryCanLogin(user, out _))
|
||||
{
|
||||
// Don't reveal that the user does not exist
|
||||
@ -775,15 +792,76 @@ namespace BTCPayServer.Controllers
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "Password successfully set."
|
||||
Message = hasPassword ? "Password successfully set." : "Account successfully created."
|
||||
});
|
||||
return RedirectToAction(nameof(Login));
|
||||
}
|
||||
|
||||
AddErrors(result);
|
||||
model.HasPassword = await _userManager.HasPasswordAsync(user);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("/invite/{userId}/{code}")]
|
||||
public async Task<IActionResult> AcceptInvite(string userId, string code)
|
||||
{
|
||||
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(code))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByInvitationTokenAsync(userId, Uri.UnescapeDataString(code));
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var requiresEmailConfirmation = user.RequiresEmailConfirmation && !user.EmailConfirmed;
|
||||
var requiresSetPassword = !await _userManager.HasPasswordAsync(user);
|
||||
|
||||
_eventAggregator.Publish(new UserInviteAcceptedEvent
|
||||
{
|
||||
User = user,
|
||||
RequestUri = Request.GetAbsoluteRootUri()
|
||||
});
|
||||
|
||||
if (requiresEmailConfirmation)
|
||||
{
|
||||
return await RedirectToConfirmEmail(user);
|
||||
}
|
||||
if (requiresSetPassword)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Message = "Invitation accepted. Please set your password."
|
||||
});
|
||||
return await RedirectToSetPassword(user);
|
||||
}
|
||||
|
||||
// Inform user that a password has been set on account creation
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Message = "Your password has been set by the user who invited you."
|
||||
});
|
||||
|
||||
return RedirectToAction(nameof(Login), new { email = user.Email });
|
||||
}
|
||||
|
||||
private async Task<IActionResult> RedirectToConfirmEmail(ApplicationUser user)
|
||||
{
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
return RedirectToAction(nameof(ConfirmEmail), new { userId = user.Id, code });
|
||||
}
|
||||
|
||||
private async Task<IActionResult> RedirectToSetPassword(ApplicationUser user)
|
||||
{
|
||||
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||
return RedirectToAction(nameof(SetPassword), new { userId = user.Id, email = user.Email, code });
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
private void AddErrors(IdentityResult result)
|
||||
|
@ -29,7 +29,6 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
private readonly ThemeSettings _theme;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private IHttpClientFactory HttpClientFactory { get; }
|
||||
private SignInManager<ApplicationUser> SignInManager { get; }
|
||||
|
||||
@ -41,14 +40,12 @@ namespace BTCPayServer.Controllers
|
||||
ThemeSettings theme,
|
||||
LanguageService languageService,
|
||||
StoreRepository storeRepository,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
IWebHostEnvironment environment,
|
||||
SignInManager<ApplicationUser> signInManager)
|
||||
{
|
||||
_theme = theme;
|
||||
HttpClientFactory = httpClientFactory;
|
||||
LanguageService = languageService;
|
||||
_networkProvider = networkProvider;
|
||||
_storeRepository = storeRepository;
|
||||
SignInManager = signInManager;
|
||||
_WebRootFileProvider = environment.WebRootFileProvider;
|
||||
@ -76,17 +73,17 @@ namespace BTCPayServer.Controllers
|
||||
if (storeId != null)
|
||||
{
|
||||
// verify store exists and redirect to it
|
||||
var store = await _storeRepository.FindStore(storeId, userId);
|
||||
var store = await _storeRepository.FindStore(storeId);
|
||||
if (store != null)
|
||||
{
|
||||
return RedirectToStore(userId, store);
|
||||
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId });
|
||||
}
|
||||
}
|
||||
|
||||
var stores = await _storeRepository.GetStoresByUserId(userId);
|
||||
var stores = await _storeRepository.GetStoresByUserId(userId!);
|
||||
var activeStore = stores.FirstOrDefault(s => !s.Archived);
|
||||
return activeStore != null
|
||||
? RedirectToStore(userId, activeStore)
|
||||
? RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId = activeStore.Id })
|
||||
: RedirectToAction(nameof(UIUserStoresController.CreateStore), "UIUserStores");
|
||||
}
|
||||
|
||||
@ -198,14 +195,5 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
|
||||
}
|
||||
public static RedirectToActionResult RedirectToStore(string userId, StoreData store)
|
||||
{
|
||||
var perms = store.GetPermissionSet(userId);
|
||||
if (perms.Contains(Policies.CanModifyStoreSettings, store.Id))
|
||||
return new RedirectToActionResult("Dashboard", "UIStores", new {storeId = store.Id});
|
||||
if (perms.Contains(Policies.CanViewInvoices, store.Id))
|
||||
return new RedirectToActionResult("ListInvoices", "UIInvoice", new { storeId = store.Id });
|
||||
return new RedirectToActionResult("Index", "UIStores", new {storeId = store.Id});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1144,7 +1144,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet("/stores/{storeId}/invoices/create")]
|
||||
[HttpGet("invoices/create")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> CreateInvoice(InvoicesModel? model = null)
|
||||
{
|
||||
@ -1154,7 +1154,7 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
|
||||
}
|
||||
|
||||
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
|
||||
var store = await _StoreRepository.FindStore(model.StoreId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
|
@ -50,7 +50,7 @@ namespace BTCPayServer.Controllers
|
||||
$"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
|
||||
}
|
||||
|
||||
_logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id);
|
||||
_logger.LogInformation("User {Email} has disabled 2fa", user.Email);
|
||||
return RedirectToAction(nameof(TwoFactorAuthentication));
|
||||
}
|
||||
|
||||
@ -100,7 +100,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
await _userManager.SetTwoFactorEnabledAsync(user, true);
|
||||
_logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id);
|
||||
_logger.LogInformation("User {Email} has enabled 2FA with an authenticator app", user.Email);
|
||||
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
|
||||
TempData[RecoveryCodesKey] = recoveryCodes.ToArray();
|
||||
|
||||
@ -117,7 +117,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
await _userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||
_logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id);
|
||||
_logger.LogInformation("User {Email} has reset their authentication app key", user.Email);
|
||||
|
||||
return RedirectToAction(nameof(EnableAuthenticator));
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ namespace BTCPayServer.Controllers
|
||||
new List<string>();
|
||||
var notifications = notificationHandlers.SelectMany(handler => handler.Meta.Select(tuple =>
|
||||
new SelectListItem(tuple.name, tuple.identifier,
|
||||
disabledNotifications.Contains(tuple.identifier, StringComparer.InvariantCultureIgnoreCase))))
|
||||
!disabledNotifications.Contains(tuple.identifier, StringComparer.InvariantCultureIgnoreCase))))
|
||||
.ToList();
|
||||
|
||||
return View(new NotificationSettingsViewModel { DisabledNotifications = notifications });
|
||||
@ -46,7 +46,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
else if (command == "update")
|
||||
{
|
||||
var disabled = vm.DisabledNotifications.Where(item => item.Selected).Select(item => item.Value)
|
||||
var disabled = vm.DisabledNotifications.Where(item => !item.Selected).Select(item => item.Value)
|
||||
.ToArray();
|
||||
user.DisabledNotifications = disabled.Any()
|
||||
? string.Join(';', disabled) + ";"
|
||||
|
@ -108,7 +108,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("/stores/{storeId}/payment-requests/edit/{payReqId?}")]
|
||||
[Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanModifyPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> EditPaymentRequest(string storeId, string payReqId)
|
||||
{
|
||||
var store = GetCurrentStore();
|
||||
@ -277,6 +277,10 @@ namespace BTCPayServer.Controllers
|
||||
if (FormDataService.Validate(form, ModelState))
|
||||
{
|
||||
prBlob.FormResponse = FormDataService.GetValues(form);
|
||||
if(string.IsNullOrEmpty(prBlob.Email) && form.GetFieldByFullName("buyerEmail") is { } emailField)
|
||||
{
|
||||
prBlob.Email = emailField.Value;
|
||||
}
|
||||
result.SetBlob(prBlob);
|
||||
await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(result);
|
||||
return RedirectToAction("PayPaymentRequest", new { payReqId });
|
||||
|
@ -11,6 +11,7 @@ using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers.Greenfield;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
@ -127,13 +128,27 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (_pullPaymentHostedService.SupportsLNURL(blob))
|
||||
{
|
||||
var url = Url.Action("GetLNURLForPullPayment", "UILNURL", new { cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, pullPaymentId = vm.Id }, Request.Scheme, Request.Host.ToString());
|
||||
var url = Url.Action(nameof(UILNURLController.GetLNURLForPullPayment), "UILNURL", new { cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, pullPaymentId = vm.Id }, Request.Scheme, Request.Host.ToString());
|
||||
vm.LnurlEndpoint = url != null ? new Uri(url) : null;
|
||||
vm.SetupDeepLink = $"boltcard://program?url={GetBoltcardDeeplinkUrl(vm, OnExistingBehavior.UpdateVersion)}";
|
||||
vm.ResetDeepLink = $"boltcard://reset?url={GetBoltcardDeeplinkUrl(vm, OnExistingBehavior.KeepVersion)}";
|
||||
}
|
||||
|
||||
return View(nameof(ViewPullPayment), vm);
|
||||
}
|
||||
|
||||
private string GetBoltcardDeeplinkUrl(ViewPullPaymentModel vm, OnExistingBehavior onExisting)
|
||||
{
|
||||
var registerUrl = Url.Action(nameof(GreenfieldPullPaymentController.RegisterBoltcard), "GreenfieldPullPayment",
|
||||
new
|
||||
{
|
||||
pullPaymentId = vm.Id,
|
||||
onExisting = onExisting.ToString()
|
||||
}, Request.Scheme, Request.Host.ToString());
|
||||
registerUrl = Uri.EscapeDataString(registerUrl);
|
||||
return registerUrl;
|
||||
}
|
||||
|
||||
[HttpGet("stores/{storeId}/pull-payments/edit/{pullPaymentId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> EditPullPayment(string storeId, string pullPaymentId)
|
||||
|
@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
@ -16,29 +15,25 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("server/roles")]
|
||||
public async Task<IActionResult> ListRoles(
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
RolesViewModel model,
|
||||
string sortOrder = null
|
||||
)
|
||||
{
|
||||
var roles = await _StoreRepository.GetStoreRoles(null, true);
|
||||
var defaultRole = (await _StoreRepository.GetDefaultRole()).Role;
|
||||
model ??= new RolesViewModel();
|
||||
model.DefaultRole = defaultRole;
|
||||
|
||||
model.DefaultRole = (await storeRepository.GetDefaultRole()).Role;
|
||||
var roles = await storeRepository.GetStoreRoles(null);
|
||||
|
||||
if (sortOrder != null)
|
||||
switch (sortOrder)
|
||||
{
|
||||
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;
|
||||
}
|
||||
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();
|
||||
@ -47,32 +42,26 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("server/roles/{role}")]
|
||||
public async Task<IActionResult> CreateOrEditRole(
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
public async Task<IActionResult> CreateOrEditRole(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
|
||||
{
|
||||
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(role));
|
||||
if (roleData == null)
|
||||
return NotFound();
|
||||
return View(new UpdateRoleViewModel()
|
||||
{
|
||||
Policies = roleData.Permissions,
|
||||
Role = roleData.Role
|
||||
});
|
||||
}
|
||||
Policies = roleData.Permissions,
|
||||
Role = roleData.Role
|
||||
});
|
||||
}
|
||||
[HttpPost("server/roles/{role}")]
|
||||
public async Task<IActionResult> CreateOrEditRole(
|
||||
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
[FromRoute] string role, UpdateRoleViewModel viewModel)
|
||||
public async Task<IActionResult> CreateOrEditRole([FromRoute] string role, UpdateRoleViewModel viewModel)
|
||||
{
|
||||
string successMessage = null;
|
||||
if (role == "create")
|
||||
@ -83,7 +72,7 @@ namespace BTCPayServer.Controllers
|
||||
else
|
||||
{
|
||||
successMessage = "Role updated";
|
||||
var storeRole = await storeRepository.GetStoreRole(new StoreRoleId(role));
|
||||
var storeRole = await _StoreRepository.GetStoreRole(new StoreRoleId(role));
|
||||
if (storeRole == null)
|
||||
return NotFound();
|
||||
}
|
||||
@ -93,7 +82,7 @@ namespace BTCPayServer.Controllers
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
var r = await storeRepository.AddOrUpdateStoreRole(new StoreRoleId(role), viewModel.Policies);
|
||||
var r = await _StoreRepository.AddOrUpdateStoreRole(new StoreRoleId(role), viewModel.Policies);
|
||||
if (r is null)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
@ -116,11 +105,9 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
|
||||
[HttpGet("server/roles/{role}/delete")]
|
||||
public async Task<IActionResult> DeleteRole(
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
public async Task<IActionResult> DeleteRole(string role)
|
||||
{
|
||||
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(role), true);
|
||||
var roleData = await _StoreRepository.GetStoreRole(new StoreRoleId(role), true);
|
||||
if (roleData == null)
|
||||
return NotFound();
|
||||
|
||||
@ -134,12 +121,10 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("server/roles/{role}/delete")]
|
||||
public async Task<IActionResult> DeleteRolePost(
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
public async Task<IActionResult> DeleteRolePost(string role)
|
||||
{
|
||||
var roleId = new StoreRoleId(role);
|
||||
var roleData = await storeRepository.GetStoreRole(roleId, true);
|
||||
var roleData = await _StoreRepository.GetStoreRole(roleId, true);
|
||||
if (roleData == null)
|
||||
return NotFound();
|
||||
if (roleData.IsUsed is true)
|
||||
@ -147,7 +132,7 @@ namespace BTCPayServer.Controllers
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var errorMessage = await storeRepository.RemoveStoreRole(roleId);
|
||||
var errorMessage = await _StoreRepository.RemoveStoreRole(roleId);
|
||||
if (errorMessage is null)
|
||||
{
|
||||
|
||||
@ -162,19 +147,16 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("server/roles/{role}/default")]
|
||||
public async Task<IActionResult> SetDefaultRole(
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
public async Task<IActionResult> SetDefaultRole(string role)
|
||||
{
|
||||
var resolved = await storeRepository.ResolveStoreRoleId(null, 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);
|
||||
await _StoreRepository.SetDefaultRole(role);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Role set default";
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -102,7 +103,7 @@ namespace BTCPayServer.Controllers
|
||||
bool? adminStatusChanged = null;
|
||||
bool? approvalStatusChanged = null;
|
||||
|
||||
if (user.RequiresApproval && viewModel.Approved.HasValue)
|
||||
if (user.RequiresApproval && viewModel.Approved.HasValue && user.Approved != viewModel.Approved.Value)
|
||||
{
|
||||
approvalStatusChanged = await _userService.SetUserApproval(user.Id, viewModel.Approved.Value, Request.GetAbsoluteRootUri());
|
||||
}
|
||||
@ -149,7 +150,6 @@ namespace BTCPayServer.Controllers
|
||||
[HttpGet("server/users/new")]
|
||||
public IActionResult CreateUser()
|
||||
{
|
||||
ViewData["AllowRequestApproval"] = _policiesSettings.RequiresUserApproval;
|
||||
ViewData["AllowRequestEmailConfirmation"] = _policiesSettings.RequiresConfirmedEmail;
|
||||
return View();
|
||||
}
|
||||
@ -157,13 +157,11 @@ namespace BTCPayServer.Controllers
|
||||
[HttpPost("server/users/new")]
|
||||
public async Task<IActionResult> CreateUser(RegisterFromAdminViewModel model)
|
||||
{
|
||||
ViewData["AllowRequestApproval"] = _policiesSettings.RequiresUserApproval;
|
||||
ViewData["AllowRequestEmailConfirmation"] = _policiesSettings.RequiresConfirmedEmail;
|
||||
if (!_Options.CheatMode)
|
||||
model.IsAdmin = false;
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
IdentityResult result;
|
||||
var user = new ApplicationUser
|
||||
{
|
||||
UserName = model.Email,
|
||||
@ -171,18 +169,13 @@ namespace BTCPayServer.Controllers
|
||||
EmailConfirmed = model.EmailConfirmed,
|
||||
RequiresEmailConfirmation = _policiesSettings.RequiresConfirmedEmail,
|
||||
RequiresApproval = _policiesSettings.RequiresUserApproval,
|
||||
Approved = model.Approved,
|
||||
Approved = true, // auto-approve users created by an admin
|
||||
Created = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(model.Password))
|
||||
{
|
||||
result = await _UserManager.CreateAsync(user, model.Password);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = await _UserManager.CreateAsync(user);
|
||||
}
|
||||
var result = string.IsNullOrEmpty(model.Password)
|
||||
? await _UserManager.CreateAsync(user)
|
||||
: await _UserManager.CreateAsync(user, model.Password);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
@ -190,37 +183,30 @@ namespace BTCPayServer.Controllers
|
||||
model.IsAdmin = false;
|
||||
|
||||
var tcs = new TaskCompletionSource<Uri>();
|
||||
var currentUser = await _UserManager.GetUserAsync(HttpContext.User);
|
||||
|
||||
_eventAggregator.Publish(new UserRegisteredEvent()
|
||||
_eventAggregator.Publish(new UserRegisteredEvent
|
||||
{
|
||||
RequestUri = Request.GetAbsoluteRootUri(),
|
||||
Kind = UserRegisteredEventKind.Invite,
|
||||
User = user,
|
||||
Admin = model.IsAdmin is true,
|
||||
InvitedByUser = currentUser,
|
||||
Admin = model.IsAdmin,
|
||||
CallbackUrlGenerated = tcs
|
||||
});
|
||||
|
||||
var callbackUrl = await tcs.Task;
|
||||
|
||||
if (user.RequiresEmailConfirmation && !user.EmailConfirmed)
|
||||
var settings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
||||
var info = settings.IsComplete()
|
||||
? "An invitation email has been sent.<br/>You may alternatively"
|
||||
: "An invitation email has not been sent, because the server does not have an email server configured.<br/> You need to";
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
AllowDismiss = false,
|
||||
Html =
|
||||
$"Account created without a set password. An email will be sent (if configured) to set the password.<br/> You may alternatively share this link with them: <a class='alert-link' href='{callbackUrl}'>{callbackUrl}</a>"
|
||||
});
|
||||
}
|
||||
else if (!await _UserManager.HasPasswordAsync(user))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
AllowDismiss = false,
|
||||
Html =
|
||||
$"Account created without a set password. An email will be sent (if configured) to set the password.<br/> You may alternatively share this link with them: <a class='alert-link' href='{callbackUrl}'>{callbackUrl}</a>"
|
||||
});
|
||||
}
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
AllowDismiss = false,
|
||||
Html = $"Account successfully created. {info} share this link with them: <a class='alert-link' href='{callbackUrl}'>{callbackUrl}</a>"
|
||||
});
|
||||
return RedirectToAction(nameof(ListUsers));
|
||||
}
|
||||
|
||||
@ -377,8 +363,5 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[Display(Name = "Email confirmed?")]
|
||||
public bool EmailConfirmed { get; set; }
|
||||
|
||||
[Display(Name = "User approved?")]
|
||||
public bool Approved { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -16,9 +16,7 @@ using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
@ -26,10 +24,8 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Storage.Models;
|
||||
using BTCPayServer.Storage.Services;
|
||||
using BTCPayServer.Storage.Services.Providers;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@ -1053,11 +1049,22 @@ namespace BTCPayServer.Controllers
|
||||
vm.LogoFileId = theme.LogoFileId;
|
||||
vm.CustomThemeFileId = theme.CustomThemeFileId;
|
||||
|
||||
if (server.ServerName != vm.ServerName || server.ContactUrl != vm.ContactUrl)
|
||||
if (server.ServerName != vm.ServerName)
|
||||
{
|
||||
server.ServerName = vm.ServerName;
|
||||
server.ContactUrl = vm.ContactUrl;
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
if (server.ContactUrl != vm.ContactUrl)
|
||||
{
|
||||
server.ContactUrl = !string.IsNullOrWhiteSpace(vm.ContactUrl)
|
||||
? vm.ContactUrl.IsValidEmail() ? $"mailto:{vm.ContactUrl}" : vm.ContactUrl
|
||||
: null;
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
if (settingsChanged)
|
||||
{
|
||||
await _SettingsRepository.UpdateSetting(server);
|
||||
}
|
||||
|
||||
@ -1180,7 +1187,7 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[Route("server/emails")]
|
||||
[HttpGet("server/emails")]
|
||||
public async Task<IActionResult> Emails()
|
||||
{
|
||||
var email = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
||||
@ -1191,8 +1198,7 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[Route("server/emails")]
|
||||
[HttpPost]
|
||||
[HttpPost("server/emails")]
|
||||
public async Task<IActionResult> Emails(ServerEmailsViewModel model, string command)
|
||||
{
|
||||
if (command == "Test")
|
||||
@ -1209,8 +1215,10 @@ namespace BTCPayServer.Controllers
|
||||
ModelState.AddModelError(nameof(model.TestEmail), new RequiredAttribute().FormatErrorMessage(nameof(model.TestEmail)));
|
||||
if (!ModelState.IsValid)
|
||||
return View(model);
|
||||
var serverSettings = await _SettingsRepository.GetSettingAsync<ServerSettings>();
|
||||
var serverName = string.IsNullOrEmpty(serverSettings?.ServerName) ? "BTCPay Server" : serverSettings.ServerName;
|
||||
using (var client = await model.Settings.CreateSmtpClient())
|
||||
using (var message = model.Settings.CreateMailMessage(MailboxAddress.Parse(model.TestEmail), "BTCPay test", "BTCPay test", false))
|
||||
using (var message = model.Settings.CreateMailMessage(MailboxAddress.Parse(model.TestEmail), $"{serverName}: Email test", "You received it, the BTCPay Server SMTP settings work.", false))
|
||||
{
|
||||
await client.SendAsync(message);
|
||||
await client.DisconnectAsync(true);
|
||||
@ -1246,7 +1254,7 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
var oldSettings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
||||
if (new EmailsViewModel(oldSettings).PasswordSet)
|
||||
if (new ServerEmailsViewModel(oldSettings).PasswordSet)
|
||||
{
|
||||
model.Settings.Password = oldSettings.Password;
|
||||
}
|
||||
|
@ -10,10 +10,9 @@ using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -23,7 +22,6 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
|
||||
using PayoutData = BTCPayServer.Data.PayoutData;
|
||||
using PullPaymentData = BTCPayServer.Data.PullPaymentData;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
@ -40,6 +38,8 @@ namespace BTCPayServer.Controllers
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly PayoutProcessorService _payoutProcessorService;
|
||||
private readonly IEnumerable<IPayoutProcessorFactory> _payoutProcessorFactories;
|
||||
|
||||
public StoreData CurrentStore
|
||||
{
|
||||
@ -55,6 +55,8 @@ namespace BTCPayServer.Controllers
|
||||
DisplayFormatter displayFormatter,
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
PayoutProcessorService payoutProcessorService,
|
||||
IEnumerable<IPayoutProcessorFactory> payoutProcessorFactories,
|
||||
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
@ -66,8 +68,10 @@ namespace BTCPayServer.Controllers
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_jsonSerializerSettings = jsonSerializerSettings;
|
||||
_authorizationService = authorizationService;
|
||||
_payoutProcessorService = payoutProcessorService;
|
||||
_payoutProcessorFactories = payoutProcessorFactories;
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("stores/{storeId}/pull-payments/new")]
|
||||
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> NewPullPayment(string storeId)
|
||||
@ -83,7 +87,7 @@ namespace BTCPayServer.Controllers
|
||||
Message = "You must enable at least one payment method before creating a pull payment.",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
});
|
||||
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId });
|
||||
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId });
|
||||
}
|
||||
|
||||
return View(new NewPullPaymentModel
|
||||
@ -161,6 +165,7 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(PullPayments), new { storeId = storeId });
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpGet("stores/{storeId}/pull-payments")]
|
||||
public async Task<IActionResult> PullPayments(
|
||||
string storeId,
|
||||
@ -199,7 +204,7 @@ namespace BTCPayServer.Controllers
|
||||
Message = "You must enable at least one payment method before creating a pull payment.",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
});
|
||||
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId });
|
||||
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId });
|
||||
}
|
||||
|
||||
var vm = this.ParseListQuery(new PullPaymentsModel
|
||||
@ -286,6 +291,7 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
|
||||
vm.PaymentMethods = await _payoutHandlers.GetSupportedPaymentMethods(HttpContext.GetStoreData());
|
||||
vm.HasPayoutProcessor = await HasPayoutProcessor(storeId, vm.PaymentMethodId);
|
||||
var paymentMethodId = PaymentMethodId.Parse(vm.PaymentMethodId);
|
||||
var handler = _payoutHandlers
|
||||
.FindPayoutHandler(paymentMethodId);
|
||||
@ -369,7 +375,7 @@ namespace BTCPayServer.Controllers
|
||||
break;
|
||||
}
|
||||
|
||||
if (command == "approve-pay")
|
||||
if (command == "approve-pay" && !vm.HasPayoutProcessor)
|
||||
{
|
||||
goto case "pay";
|
||||
}
|
||||
@ -482,19 +488,21 @@ namespace BTCPayServer.Controllers
|
||||
Message = "You must enable at least one payment method before creating a payout.",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
});
|
||||
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId });
|
||||
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId });
|
||||
}
|
||||
|
||||
paymentMethodId ??= paymentMethods.First().ToString();
|
||||
var vm = this.ParseListQuery(new PayoutsModel
|
||||
{
|
||||
PaymentMethods = paymentMethods,
|
||||
PaymentMethodId = paymentMethodId ?? paymentMethods.First().ToString(),
|
||||
PaymentMethodId = paymentMethodId,
|
||||
PullPaymentId = pullPaymentId,
|
||||
PayoutState = payoutState,
|
||||
Skip = skip,
|
||||
Count = count
|
||||
Count = count,
|
||||
Payouts = new List<PayoutsModel.PayoutModel>(),
|
||||
HasPayoutProcessor = await HasPayoutProcessor(storeId, paymentMethodId)
|
||||
});
|
||||
vm.Payouts = new List<PayoutsModel.PayoutModel>();
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
var payoutRequest =
|
||||
ctx.Payouts.Where(p => p.StoreDataId == storeId && (p.PullPaymentDataId == null || !p.PullPaymentData.Archived));
|
||||
@ -576,5 +584,13 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private async Task<bool> HasPayoutProcessor(string storeId, string paymentMethodId)
|
||||
{
|
||||
var pmId = PaymentMethodId.Parse(paymentMethodId);
|
||||
var processors = await _payoutProcessorService.GetProcessors(
|
||||
new PayoutProcessorService.PayoutProcessorQuery { Stores = [storeId], PaymentMethods = [paymentMethodId] });
|
||||
return _payoutProcessorFactories.Any(factory => factory.GetSupportedPaymentMethods().Contains(pmId)) && processors.Any();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,15 @@
|
||||
#nullable enable
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Components.StoreLightningBalance;
|
||||
using BTCPayServer.Components.StoreNumbers;
|
||||
using BTCPayServer.Components.StoreRecentInvoices;
|
||||
using BTCPayServer.Components.StoreRecentTransactions;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
@ -14,9 +17,13 @@ namespace BTCPayServer.Controllers
|
||||
public partial class UIStoresController
|
||||
{
|
||||
[HttpGet("{storeId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Dashboard()
|
||||
{
|
||||
var store = CurrentStore;
|
||||
if (store is null)
|
||||
return NotFound();
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
|
||||
AddPaymentMethods(store, storeBlob,
|
||||
@ -38,16 +45,17 @@ namespace BTCPayServer.Controllers
|
||||
};
|
||||
|
||||
// Widget data
|
||||
if (!vm.WalletEnabled && !vm.LightningEnabled)
|
||||
if (vm is { WalletEnabled: false, LightningEnabled: false })
|
||||
return View(vm);
|
||||
|
||||
var userId = GetUserId();
|
||||
if (userId is null)
|
||||
return NotFound();
|
||||
|
||||
var apps = await _appService.GetAllApps(userId, false, store.Id);
|
||||
foreach (var app in apps)
|
||||
{
|
||||
var appData = await _appService.GetAppDataIfOwner(userId, app.Id);
|
||||
var appData = await _appService.GetAppData(userId, app.Id);
|
||||
vm.Apps.Add(appData);
|
||||
}
|
||||
|
||||
@ -55,6 +63,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/dashboard/{cryptoCode}/lightning/balance")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult LightningBalance(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
@ -66,6 +75,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/dashboard/{cryptoCode}/numbers")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult StoreNumbers(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
@ -77,6 +87,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/dashboard/{cryptoCode}/recent-transactions")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult RecentTransactions(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
@ -88,6 +99,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/dashboard/{cryptoCode}/recent-invoices")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult RecentInvoices(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
|
@ -7,9 +7,11 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MimeKit;
|
||||
|
||||
@ -43,6 +45,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/emails")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> StoreEmails(string storeId, StoreEmailRuleViewModel vm, string command)
|
||||
{
|
||||
vm.Rules ??= new List<StoreEmailRule>();
|
||||
@ -118,7 +121,7 @@ namespace BTCPayServer.Controllers
|
||||
.Where(o => o != null)
|
||||
.ToArray();
|
||||
|
||||
emailSender.SendEmail(recipients.ToArray(), null, null, $"({store.StoreName} test) {rule.Subject}", rule.Body);
|
||||
emailSender.SendEmail(recipients.ToArray(), null, null, $"[TEST] {rule.Subject}", rule.Body);
|
||||
message += "Test email sent — please verify you received it.";
|
||||
}
|
||||
else
|
||||
@ -185,33 +188,40 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/email-settings")]
|
||||
public async Task<IActionResult> StoreEmailSettings(string storeId, EmailsViewModel model, string command)
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> StoreEmailSettings(string storeId, EmailsViewModel model, string command, [FromForm] bool useCustomSMTP = false)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var emailSender = await _emailSenderFactory.GetEmailSender(store.Id) as StoreEmailSender;
|
||||
var fallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender
|
||||
|
||||
ViewBag.UseCustomSMTP = useCustomSMTP;
|
||||
model.FallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender
|
||||
? await storeSender.FallbackSender.GetEmailSettings()
|
||||
: null;
|
||||
model.FallbackSettings = fallbackSettings;
|
||||
|
||||
if (useCustomSMTP)
|
||||
{
|
||||
model.Settings.Validate("Settings.", ModelState);
|
||||
}
|
||||
if (command == "Test")
|
||||
{
|
||||
try
|
||||
{
|
||||
if (model.PasswordSet)
|
||||
if (useCustomSMTP)
|
||||
{
|
||||
model.Settings.Password = store.GetStoreBlob().EmailSettings.Password;
|
||||
if (model.PasswordSet)
|
||||
{
|
||||
model.Settings.Password = store.GetStoreBlob().EmailSettings.Password;
|
||||
}
|
||||
}
|
||||
model.Settings.Validate("Settings.", ModelState);
|
||||
|
||||
if (string.IsNullOrEmpty(model.TestEmail))
|
||||
ModelState.AddModelError(nameof(model.TestEmail), new RequiredAttribute().FormatErrorMessage(nameof(model.TestEmail)));
|
||||
if (!ModelState.IsValid)
|
||||
return View(model);
|
||||
using var client = await model.Settings.CreateSmtpClient();
|
||||
var message = model.Settings.CreateMailMessage(MailboxAddress.Parse(model.TestEmail), "BTCPay test", "BTCPay test", false);
|
||||
var settings = useCustomSMTP ? model.Settings : model.FallbackSettings;
|
||||
using var client = await settings.CreateSmtpClient();
|
||||
var message = settings.CreateMailMessage(MailboxAddress.Parse(model.TestEmail), $"{store.StoreName}: Email test", "You received it, the BTCPay Server SMTP settings work.", false);
|
||||
await client.SendAsync(message);
|
||||
await client.DisconnectAsync(true);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Email sent to {model.TestEmail}. Please verify you received it.";
|
||||
@ -229,17 +239,17 @@ namespace BTCPayServer.Controllers
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _Repo.UpdateStore(store);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Email server password reset";
|
||||
return RedirectToAction(nameof(StoreEmailSettings), new { storeId });
|
||||
}
|
||||
else // if (command == "Save")
|
||||
if (useCustomSMTP)
|
||||
{
|
||||
if (model.Settings.From is not null && !MailboxAddressValidator.IsMailboxAddress(model.Settings.From))
|
||||
{
|
||||
ModelState.AddModelError("Settings.From", "Invalid email");
|
||||
return View(model);
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return View(model);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
if (storeBlob.EmailSettings != null && new EmailsViewModel(storeBlob.EmailSettings, fallbackSettings).PasswordSet)
|
||||
if (storeBlob.EmailSettings != null && new EmailsViewModel(storeBlob.EmailSettings, model.FallbackSettings).PasswordSet)
|
||||
{
|
||||
model.Settings.Password = storeBlob.EmailSettings.Password;
|
||||
}
|
||||
@ -247,8 +257,8 @@ namespace BTCPayServer.Controllers
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _Repo.UpdateStore(store);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Email settings modified";
|
||||
return RedirectToAction(nameof(StoreEmailSettings), new { storeId });
|
||||
}
|
||||
return RedirectToAction(nameof(StoreEmailSettings), new { storeId });
|
||||
}
|
||||
|
||||
private static async Task<bool> IsSetupComplete(IEmailSender emailSender)
|
||||
|
@ -4,10 +4,12 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
@ -46,6 +48,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks/new")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult NewWebhook()
|
||||
{
|
||||
return View(nameof(ModifyWebhook), new EditWebhookViewModel
|
||||
@ -58,6 +61,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks/{webhookId}/remove")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> DeleteWebhook(string webhookId)
|
||||
{
|
||||
var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
@ -68,6 +72,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/{webhookId}/remove")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> DeleteWebhookPost(string webhookId)
|
||||
{
|
||||
var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
@ -80,6 +85,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/new")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> NewWebhook(string storeId, EditWebhookViewModel viewModel)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
@ -91,6 +97,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks/{webhookId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ModifyWebhook(string webhookId)
|
||||
{
|
||||
var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
@ -107,6 +114,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/{webhookId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ModifyWebhook(string webhookId, EditWebhookViewModel viewModel)
|
||||
{
|
||||
var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
@ -121,6 +129,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks/{webhookId}/test")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> TestWebhook(string webhookId)
|
||||
{
|
||||
var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
@ -131,6 +140,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/{webhookId}/test")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> TestWebhook(string webhookId, TestWebhookViewModel viewModel, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await WebhookNotificationManager.TestWebhook(CurrentStore.Id, webhookId, viewModel.Type, cancellationToken);
|
||||
@ -148,6 +158,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> RedeliverWebhook(string webhookId, string deliveryId)
|
||||
{
|
||||
var delivery = await _Repo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId);
|
||||
@ -168,6 +179,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> WebhookDelivery(string webhookId, string deliveryId)
|
||||
{
|
||||
var delivery = await _Repo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId);
|
||||
|
@ -5,7 +5,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Components.StoreLightningBalance;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Lightning;
|
||||
@ -14,7 +14,7 @@ using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@ -22,8 +22,8 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class UIStoresController
|
||||
{
|
||||
|
||||
[HttpGet("{storeId}/lightning/{cryptoCode}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult Lightning(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
@ -85,6 +85,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/lightning/{cryptoCode}/setup")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult SetupLightningNode(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
@ -101,6 +102,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/lightning/{cryptoCode}/setup")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> SetupLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
|
||||
{
|
||||
vm.CryptoCode = cryptoCode;
|
||||
@ -217,6 +219,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/lightning/{cryptoCode}/settings")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult LightningSettings(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
@ -257,6 +260,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/lightning/{cryptoCode}/settings")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> LightningSettings(LightningSettingsViewModel vm)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
@ -310,6 +314,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/lightning/{cryptoCode}/status")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> SetLightningNodeEnabled(string storeId, string cryptoCode, bool enabled)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@ -8,12 +7,12 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
@ -27,6 +26,7 @@ namespace BTCPayServer.Controllers
|
||||
public partial class UIStoresController
|
||||
{
|
||||
[HttpGet("{storeId}/onchain/{cryptoCode}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public ActionResult SetupWallet(WalletSetupViewModel vm)
|
||||
{
|
||||
var checkResult = IsAvailable(vm.CryptoCode, out var store, out _);
|
||||
@ -42,6 +42,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/onchain/{cryptoCode}/import/{method?}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ImportWallet(WalletSetupViewModel vm)
|
||||
{
|
||||
var checkResult = IsAvailable(vm.CryptoCode, out _, out var network);
|
||||
@ -71,6 +72,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost("{storeId}/onchain/{cryptoCode}/modify")]
|
||||
[HttpPost("{storeId}/onchain/{cryptoCode}/import/{method}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> UpdateWallet(WalletSetupViewModel vm)
|
||||
{
|
||||
var checkResult = IsAvailable(vm.CryptoCode, out var store, out var network);
|
||||
@ -197,6 +199,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/onchain/{cryptoCode}/generate/{method?}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> GenerateWallet(WalletSetupViewModel vm)
|
||||
{
|
||||
var checkResult = IsAvailable(vm.CryptoCode, out _, out var network);
|
||||
@ -235,8 +238,11 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
return View(vm.ViewName, vm);
|
||||
}
|
||||
|
||||
internal GenerateWalletResponse GenerateWalletResponse;
|
||||
|
||||
[HttpPost("{storeId}/onchain/{cryptoCode}/generate/{method}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> GenerateWallet(string storeId, string cryptoCode, WalletSetupMethod method, WalletSetupRequest request)
|
||||
{
|
||||
var checkResult = IsAvailable(cryptoCode, out _, out var network);
|
||||
@ -356,6 +362,7 @@ namespace BTCPayServer.Controllers
|
||||
// The purpose of this action is to show the user a success message, which confirms
|
||||
// that the store settings have been updated after generating a new wallet.
|
||||
[HttpGet("{storeId}/onchain/{cryptoCode}/generate/confirm")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public ActionResult GenerateWalletConfirm(string storeId, string cryptoCode)
|
||||
{
|
||||
var checkResult = IsAvailable(cryptoCode, out _, out var network);
|
||||
@ -371,6 +378,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/onchain/{cryptoCode}/settings")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> WalletSettings(string storeId, string cryptoCode)
|
||||
{
|
||||
var checkResult = IsAvailable(cryptoCode, out var store, out var network);
|
||||
@ -440,6 +448,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/onchain/{cryptoCode}/settings/wallet")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> UpdateWalletSettings(WalletSettingsViewModel vm)
|
||||
{
|
||||
var checkResult = IsAvailable(vm.CryptoCode, out var store, out _);
|
||||
@ -549,6 +558,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/onchain/{cryptoCode}/settings/payment")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> UpdatePaymentSettings(WalletSettingsViewModel vm)
|
||||
{
|
||||
var checkResult = IsAvailable(vm.CryptoCode, out var store, out _);
|
||||
@ -612,6 +622,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/onchain/{cryptoCode}/seed")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> WalletSeed(string storeId, string cryptoCode, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var checkResult = IsAvailable(cryptoCode, out var store, out var network);
|
||||
@ -658,6 +669,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/onchain/{cryptoCode}/replace")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public ActionResult ReplaceWallet(string storeId, string cryptoCode)
|
||||
{
|
||||
var checkResult = IsAvailable(cryptoCode, out var store, out var network);
|
||||
@ -677,6 +689,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/onchain/{cryptoCode}/replace")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult ConfirmReplaceWallet(string storeId, string cryptoCode)
|
||||
{
|
||||
var checkResult = IsAvailable(cryptoCode, out var store, out _);
|
||||
@ -695,6 +708,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/onchain/{cryptoCode}/delete")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public ActionResult DeleteWallet(string storeId, string cryptoCode)
|
||||
{
|
||||
var checkResult = IsAvailable(cryptoCode, out var store, out var network);
|
||||
@ -714,6 +728,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/onchain/{cryptoCode}/delete")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ConfirmDeleteWallet(string storeId, string cryptoCode)
|
||||
{
|
||||
var checkResult = IsAvailable(cryptoCode, out var store, out var network);
|
||||
|
@ -1,22 +1,19 @@
|
||||
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.Client;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class UIStoresController
|
||||
{
|
||||
[Route("{storeId}/roles")]
|
||||
[HttpGet("{storeId}/roles")]
|
||||
public async Task<IActionResult> ListRoles(
|
||||
string storeId,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
@ -24,24 +21,21 @@ namespace BTCPayServer.Controllers
|
||||
string sortOrder = null
|
||||
)
|
||||
{
|
||||
var roles = await storeRepository.GetStoreRoles(storeId, true);
|
||||
var defaultRole = (await storeRepository.GetDefaultRole()).Role;
|
||||
model ??= new RolesViewModel();
|
||||
model.DefaultRole = defaultRole;
|
||||
|
||||
model.DefaultRole = (await storeRepository.GetDefaultRole()).Role;
|
||||
var roles = await storeRepository.GetStoreRoles(storeId, false, false);
|
||||
|
||||
if (sortOrder != null)
|
||||
switch (sortOrder)
|
||||
{
|
||||
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;
|
||||
}
|
||||
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();
|
||||
@ -50,6 +44,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/roles/{role}")]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> CreateOrEditRole(
|
||||
string storeId,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
@ -60,19 +55,19 @@ namespace BTCPayServer.Controllers
|
||||
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
|
||||
{
|
||||
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(storeId, role));
|
||||
if (roleData == null)
|
||||
return NotFound();
|
||||
return View(new UpdateRoleViewModel()
|
||||
{
|
||||
Policies = roleData.Permissions,
|
||||
Role = roleData.Role
|
||||
});
|
||||
}
|
||||
}
|
||||
Policies = roleData.Permissions,
|
||||
Role = roleData.Role
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/roles/{role}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> CreateOrEditRole(
|
||||
string storeId,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
@ -119,10 +114,9 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
return RedirectToAction(nameof(ListRoles), new { storeId });
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpGet("{storeId}/roles/{role}/delete")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> DeleteRole(
|
||||
string storeId,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
@ -142,6 +136,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/roles/{role}/delete")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> DeleteRolePost(
|
||||
string storeId,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
|
172
BTCPayServer/Controllers/UIStoresController.Users.cs
Normal file
172
BTCPayServer/Controllers/UIStoresController.Users.cs
Normal file
@ -0,0 +1,172 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
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.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.HostedServices.Webhooks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security.Bitpay;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class UIStoresController
|
||||
{
|
||||
[HttpGet("{storeId}/users")]
|
||||
public async Task<IActionResult> StoreUsers()
|
||||
{
|
||||
var vm = new StoreUsersViewModel { Role = StoreRoleId.Employee.Role };
|
||||
await FillUsers(vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/users")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> StoreUsers(string storeId, StoreUsersViewModel vm)
|
||||
{
|
||||
await FillUsers(vm);
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
var user = await _UserManager.FindByEmailAsync(vm.Email);
|
||||
var isExistingUser = user is not null;
|
||||
var isExistingStoreUser = isExistingUser && await _Repo.GetStoreUser(storeId, user!.Id) is not null;
|
||||
var successInfo = string.Empty;
|
||||
if (user == null)
|
||||
{
|
||||
user = new ApplicationUser
|
||||
{
|
||||
UserName = vm.Email,
|
||||
Email = vm.Email,
|
||||
RequiresEmailConfirmation = _policiesSettings.RequiresConfirmedEmail,
|
||||
RequiresApproval = _policiesSettings.RequiresUserApproval,
|
||||
Created = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
var result = await _UserManager.CreateAsync(user);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<Uri>();
|
||||
var currentUser = await _UserManager.GetUserAsync(HttpContext.User);
|
||||
|
||||
_eventAggregator.Publish(new UserRegisteredEvent
|
||||
{
|
||||
RequestUri = Request.GetAbsoluteRootUri(),
|
||||
Kind = UserRegisteredEventKind.Invite,
|
||||
User = user,
|
||||
InvitedByUser = currentUser,
|
||||
CallbackUrlGenerated = tcs
|
||||
});
|
||||
|
||||
var callbackUrl = await tcs.Task;
|
||||
var settings = await _settingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
||||
var info = settings.IsComplete()
|
||||
? "An invitation email has been sent.<br/>You may alternatively"
|
||||
: "An invitation email has not been sent, because the server does not have an email server configured.<br/> You need to";
|
||||
successInfo = $"{info} share this link with them: <a class='alert-link' href='{callbackUrl}'>{callbackUrl}</a>";
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Email), "User could not be invited");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
var roleId = await _Repo.ResolveStoreRoleId(storeId, vm.Role);
|
||||
var action = isExistingUser
|
||||
? isExistingStoreUser ? "updated" : "added"
|
||||
: "invited";
|
||||
if (await _Repo.AddOrUpdateStoreUser(CurrentStore.Id, user.Id, roleId))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
AllowDismiss = false,
|
||||
Html = $"User {action} successfully." + (string.IsNullOrEmpty(successInfo) ? "" : $" {successInfo}")
|
||||
});
|
||||
return RedirectToAction(nameof(StoreUsers));
|
||||
}
|
||||
|
||||
ModelState.AddModelError(nameof(vm.Email), $"The user could not be {action}");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/users/{userId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> UpdateStoreUser(string storeId, string userId, StoreUsersViewModel.StoreUserViewModel vm)
|
||||
{
|
||||
var roleId = await _Repo.ResolveStoreRoleId(storeId, vm.Role);
|
||||
var storeUsers = await _Repo.GetStoreUsers(storeId);
|
||||
var user = storeUsers.First(user => user.Id == userId);
|
||||
var isOwner = user.StoreRole.Id == StoreRoleId.Owner.Id;
|
||||
var isLastOwner = isOwner && storeUsers.Count(u => u.StoreRole.Id == StoreRoleId.Owner.Id) == 1;
|
||||
if (isLastOwner && roleId != StoreRoleId.Owner)
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"User {user.Email} is the last owner. Their role cannot be changed.";
|
||||
else if (await _Repo.AddOrUpdateStoreUser(storeId, userId, roleId))
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"The role of {user.Email} has been changed to {vm.Role}.";
|
||||
return RedirectToAction(nameof(StoreUsers), new { storeId, userId });
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/users/{userId}/delete")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> DeleteStoreUser(string storeId, string userId)
|
||||
{
|
||||
if (await _Repo.RemoveStoreUser(storeId, userId))
|
||||
TempData[WellKnownTempData.SuccessMessage] = "User removed successfully.";
|
||||
else
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Removing this user would result in the store having no owner.";
|
||||
return RedirectToAction(nameof(StoreUsers), new { storeId, userId });
|
||||
}
|
||||
|
||||
private async Task FillUsers(StoreUsersViewModel vm)
|
||||
{
|
||||
var users = await _Repo.GetStoreUsers(CurrentStore.Id);
|
||||
vm.StoreId = CurrentStore.Id;
|
||||
vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel()
|
||||
{
|
||||
Email = u.Email,
|
||||
Id = u.Id,
|
||||
Role = u.StoreRole.Role
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
}
|
@ -8,11 +8,12 @@ using System.Threading;
|
||||
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.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.HostedServices.Webhooks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
@ -39,10 +40,9 @@ using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
|
||||
[Route("stores")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public partial class UIStoresController : Controller
|
||||
{
|
||||
@ -62,7 +62,6 @@ namespace BTCPayServer.Controllers
|
||||
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
|
||||
PoliciesSettings policiesSettings,
|
||||
IAuthorizationService authorizationService,
|
||||
EventAggregator eventAggregator,
|
||||
AppService appService,
|
||||
IFileService fileService,
|
||||
WebhookSender webhookNotificationManager,
|
||||
@ -72,7 +71,9 @@ namespace BTCPayServer.Controllers
|
||||
IHtmlHelper html,
|
||||
LightningClientFactoryService lightningClientFactoryService,
|
||||
EmailSenderFactory emailSenderFactory,
|
||||
WalletFileParsers onChainWalletParsers)
|
||||
WalletFileParsers onChainWalletParsers,
|
||||
SettingsRepository settingsRepository,
|
||||
EventAggregator eventAggregator)
|
||||
{
|
||||
_RateFactory = rateFactory;
|
||||
_Repo = repo;
|
||||
@ -99,6 +100,8 @@ namespace BTCPayServer.Controllers
|
||||
_lightningClientFactoryService = lightningClientFactoryService;
|
||||
_emailSenderFactory = emailSenderFactory;
|
||||
_onChainWalletParsers = onChainWalletParsers;
|
||||
_settingsRepository = settingsRepository;
|
||||
_eventAggregator = eventAggregator;
|
||||
Html = html;
|
||||
}
|
||||
|
||||
@ -112,6 +115,7 @@ namespace BTCPayServer.Controllers
|
||||
readonly TokenRepository _TokenRepository;
|
||||
readonly UserManager<ApplicationUser> _UserManager;
|
||||
readonly RateFetcher _RateFactory;
|
||||
readonly SettingsRepository _settingsRepository;
|
||||
private readonly ExplorerClientProvider _ExplorerProvider;
|
||||
private readonly LanguageService _LangService;
|
||||
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
|
||||
@ -124,6 +128,7 @@ namespace BTCPayServer.Controllers
|
||||
private readonly LightningClientFactoryService _lightningClientFactoryService;
|
||||
private readonly EmailSenderFactory _emailSenderFactory;
|
||||
private readonly WalletFileParsers _onChainWalletParsers;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
|
||||
public string? GeneratedPairingCode { get; set; }
|
||||
public WebhookSender WebhookNotificationManager { get; }
|
||||
@ -145,97 +150,22 @@ namespace BTCPayServer.Controllers
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
return Forbid();
|
||||
|
||||
var store = await _Repo.FindStore(storeId, userId);
|
||||
var store = await _Repo.FindStore(storeId);
|
||||
if (store is null)
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
HttpContext.SetStoreData(store);
|
||||
if (store.GetPermissionSet(userId).Contains(Policies.CanModifyStoreSettings, storeId))
|
||||
return NotFound();
|
||||
|
||||
if ((await _authorizationService.AuthorizeAsync(User, Policies.CanModifyStoreSettings)).Succeeded)
|
||||
{
|
||||
return RedirectToAction("Dashboard", new { storeId });
|
||||
}
|
||||
if (store.GetPermissionSet(userId).Contains(Policies.CanViewInvoices, storeId))
|
||||
if ((await _authorizationService.AuthorizeAsync(User, Policies.CanViewInvoices)).Succeeded)
|
||||
{
|
||||
return RedirectToAction("ListInvoices", "UIInvoice", new { storeId });
|
||||
}
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/users")]
|
||||
public async Task<IActionResult> StoreUsers()
|
||||
{
|
||||
var vm = new StoreUsersViewModel { Role = StoreRoleId.Guest.Role };
|
||||
await FillUsers(vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private async Task FillUsers(StoreUsersViewModel vm)
|
||||
{
|
||||
var users = await _Repo.GetStoreUsers(CurrentStore.Id);
|
||||
vm.StoreId = CurrentStore.Id;
|
||||
vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel()
|
||||
{
|
||||
Email = u.Email,
|
||||
Id = u.Id,
|
||||
Role = u.StoreRole.Role
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public StoreData CurrentStore => HttpContext.GetStoreData();
|
||||
|
||||
[HttpPost("{storeId}/users")]
|
||||
public async Task<IActionResult> StoreUsers(string storeId, StoreUsersViewModel vm)
|
||||
{
|
||||
await FillUsers(vm);
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
var user = await _UserManager.FindByEmailAsync(vm.Email);
|
||||
if (user == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Email), "User not found");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
TempData[WellKnownTempData.SuccessMessage] = "User added successfully.";
|
||||
return RedirectToAction(nameof(StoreUsers));
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/users/{userId}/delete")]
|
||||
public async Task<IActionResult> DeleteStoreUser(string userId)
|
||||
{
|
||||
var user = await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel("Remove store user", $"This action will prevent <strong>{Html.Encode(user.Email)}</strong> from accessing this store and its settings. Are you sure?", "Remove"));
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/users/{userId}/delete")]
|
||||
public async Task<IActionResult> DeleteStoreUserPost(string storeId, string userId)
|
||||
{
|
||||
if (await _Repo.RemoveStoreUser(storeId, userId))
|
||||
TempData[WellKnownTempData.SuccessMessage] = "User removed successfully.";
|
||||
else
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Removing this user would result in the store having no owner.";
|
||||
}
|
||||
return RedirectToAction(nameof(StoreUsers), new { storeId, userId });
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
public StoreData? CurrentStore => HttpContext.GetStoreData();
|
||||
|
||||
[HttpGet("{storeId}/rates")]
|
||||
public IActionResult Rates()
|
||||
@ -255,6 +185,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/rates")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Rates(RatesViewModel model, string? command = null, string? storeId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (command == "scripting-on")
|
||||
@ -373,6 +304,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/rates/confirm")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult ShowRateRules(bool scripting)
|
||||
{
|
||||
return View("Confirm", new ConfirmModel
|
||||
@ -387,6 +319,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/rates/confirm")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ShowRateRulesPost(bool scripting)
|
||||
{
|
||||
var blob = CurrentStore.GetStoreBlob();
|
||||
@ -487,6 +420,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/checkout")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> CheckoutAppearance(CheckoutAppearanceViewModel model, [FromForm] bool RemoveSoundFile = false)
|
||||
{
|
||||
bool needUpdate = false;
|
||||
@ -614,7 +548,7 @@ namespace BTCPayServer.Controllers
|
||||
blob.CustomLogo = model.CustomLogo;
|
||||
blob.CustomCSS = model.CustomCSS;
|
||||
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;
|
||||
blob.StoreSupportUrl = string.IsNullOrWhiteSpace(model.SupportUrl) ? null : model.SupportUrl;
|
||||
blob.StoreSupportUrl = string.IsNullOrWhiteSpace(model.SupportUrl) ? null : model.SupportUrl.IsValidEmail() ? $"mailto:{model.SupportUrl}" : model.SupportUrl;
|
||||
blob.DisplayExpirationTimer = TimeSpan.FromMinutes(model.DisplayExpirationTimer);
|
||||
blob.AutoDetectLanguage = model.AutoDetectLanguage;
|
||||
blob.DefaultLang = model.DefaultLang;
|
||||
@ -723,6 +657,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/settings")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> GeneralSettings(
|
||||
GeneralSettingsViewModel model,
|
||||
[FromForm] bool RemoveLogoFile = false,
|
||||
@ -863,7 +798,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/archive")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettings)]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ToggleArchive(string storeId)
|
||||
{
|
||||
CurrentStore.Archived = !CurrentStore.Archived;
|
||||
@ -880,12 +815,14 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/delete")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult DeleteStore(string storeId)
|
||||
{
|
||||
return View("Confirm", new ConfirmModel("Delete store", "The store will be permanently deleted. This action will also delete all invoices, apps and data associated with the store. Are you sure?", "Delete"));
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/delete")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> DeleteStorePost(string storeId)
|
||||
{
|
||||
await _Repo.DeleteStore(CurrentStore.Id);
|
||||
@ -924,6 +861,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/tokens")]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ListTokens()
|
||||
{
|
||||
var model = new TokensViewModel();
|
||||
@ -945,6 +883,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/tokens/{tokenId}/revoke")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> RevokeToken(string tokenId)
|
||||
{
|
||||
var token = await _TokenRepository.GetToken(tokenId);
|
||||
@ -954,6 +893,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/tokens/{tokenId}/revoke")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> RevokeTokenConfirm(string tokenId)
|
||||
{
|
||||
var token = await _TokenRepository.GetToken(tokenId);
|
||||
@ -967,6 +907,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/tokens/{tokenId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ShowToken(string tokenId)
|
||||
{
|
||||
var token = await _TokenRepository.GetToken(tokenId);
|
||||
@ -976,6 +917,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/tokens/create")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult CreateToken(string storeId)
|
||||
{
|
||||
var model = new CreateTokenViewModel();
|
||||
@ -987,6 +929,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/tokens/create")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> CreateToken(string storeId, CreateTokenViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
@ -1065,6 +1008,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/tokens/apikey")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> GenerateAPIKey(string storeId, string command = "")
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
@ -1129,6 +1073,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("/api-access-request")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Pair(string pairingCode, string storeId)
|
||||
{
|
||||
if (pairingCode == null)
|
||||
|
@ -6,7 +6,6 @@ using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -439,7 +439,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
var store = await Repository.FindStore(walletId.StoreId);
|
||||
var paymentMethod = GetDerivationSchemeSettings(walletId);
|
||||
if (paymentMethod == null || store is null)
|
||||
return NotFound();
|
||||
@ -459,11 +459,12 @@ namespace BTCPayServer.Controllers
|
||||
};
|
||||
if (bip21?.Any() is true)
|
||||
{
|
||||
var messagePresent = TempData.HasStatusMessage();
|
||||
foreach (var link in bip21)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(link))
|
||||
{
|
||||
await LoadFromBIP21(walletId, model, link, network);
|
||||
await LoadFromBIP21(walletId, model, link, network, messagePresent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -539,7 +540,6 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
catch (Exception ex) { model.RateError = ex.Message; }
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
@ -564,7 +564,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
var store = await Repository.FindStore(walletId.StoreId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||
@ -575,7 +575,7 @@ namespace BTCPayServer.Controllers
|
||||
if (!string.IsNullOrEmpty(bip21))
|
||||
{
|
||||
vm.Outputs?.Clear();
|
||||
await LoadFromBIP21(walletId, vm, bip21, network);
|
||||
await LoadFromBIP21(walletId, vm, bip21, network, TempData.HasStatusMessage());
|
||||
}
|
||||
|
||||
decimal transactionAmountSum = 0;
|
||||
@ -870,7 +870,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
|
||||
private async Task LoadFromBIP21(WalletId walletId, WalletSendModel vm, string bip21,
|
||||
BTCPayNetwork network)
|
||||
BTCPayNetwork network, bool statusMessagePresent)
|
||||
{
|
||||
BitcoinAddress? address = null;
|
||||
vm.Outputs ??= new();
|
||||
@ -892,14 +892,18 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
vm.Outputs.Add(output);
|
||||
address = uriBuilder.Address;
|
||||
if (!string.IsNullOrEmpty(uriBuilder.Label) || !string.IsNullOrEmpty(uriBuilder.Message))
|
||||
// only set SetStatusMessageModel if there is not message already or there is label / message in uri builder
|
||||
if (!statusMessagePresent)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
if (!string.IsNullOrEmpty(uriBuilder.Label) || !string.IsNullOrEmpty(uriBuilder.Message))
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Html =
|
||||
$"Payment {(string.IsNullOrEmpty(uriBuilder.Label) ? string.Empty : $" to <strong>{uriBuilder.Label}</strong>")} {(string.IsNullOrEmpty(uriBuilder.Message) ? string.Empty : $" for <strong>{uriBuilder.Message}</strong>")}"
|
||||
});
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Html =
|
||||
$"Payment {(string.IsNullOrEmpty(uriBuilder.Label) ? string.Empty : $" to <strong>{uriBuilder.Label}</strong>")} {(string.IsNullOrEmpty(uriBuilder.Message) ? string.Empty : $" for <strong>{uriBuilder.Message}</strong>")}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (uriBuilder.TryGetPayjoinEndpoint(out _))
|
||||
|
@ -134,7 +134,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
||||
return raw.ToObject<ManualPayoutProof>();
|
||||
}
|
||||
|
||||
public static void ParseProofType(byte[] proof, out JObject obj, out string type)
|
||||
public static void ParseProofType(string proof, out JObject obj, out string type)
|
||||
{
|
||||
type = null;
|
||||
if (proof is null)
|
||||
@ -143,7 +143,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
||||
return;
|
||||
}
|
||||
|
||||
obj = JObject.Parse(Encoding.UTF8.GetString(proof));
|
||||
obj = JObject.Parse(proof);
|
||||
TryParseProofType(obj, out type);
|
||||
}
|
||||
|
||||
|
@ -44,18 +44,18 @@ namespace BTCPayServer.Data
|
||||
|
||||
public static PayoutBlob GetBlob(this PayoutData data, BTCPayNetworkJsonSerializerSettings serializers)
|
||||
{
|
||||
var result = JsonConvert.DeserializeObject<PayoutBlob>(Encoding.UTF8.GetString(data.Blob), serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode));
|
||||
var result = JsonConvert.DeserializeObject<PayoutBlob>(data.Blob, serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode));
|
||||
result.Metadata ??= new JObject();
|
||||
return result;
|
||||
}
|
||||
public static void SetBlob(this PayoutData data, PayoutBlob blob, BTCPayNetworkJsonSerializerSettings serializers)
|
||||
{
|
||||
data.Blob = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob, serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode)));
|
||||
data.Blob = JsonConvert.SerializeObject(blob, serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode)).ToString();
|
||||
}
|
||||
|
||||
public static JObject GetProofBlobJson(this PayoutData data)
|
||||
{
|
||||
return data?.Proof is null ? null : JObject.Parse(Encoding.UTF8.GetString(data.Proof));
|
||||
return data?.Proof is null ? null : JObject.Parse(data.Proof);
|
||||
}
|
||||
public static void SetProofBlob(this PayoutData data, IPayoutProof blob, JsonSerializerSettings settings)
|
||||
{
|
||||
@ -76,11 +76,10 @@ namespace BTCPayServer.Data
|
||||
data.Proof = null;
|
||||
return;
|
||||
}
|
||||
var bytes = Encoding.UTF8.GetBytes(blob.ToString(Formatting.None));
|
||||
// We only update the property if the bytes actually changed, this prevent from hammering the DB too much
|
||||
if (data.Proof is null || bytes.Length != data.Proof.Length || !bytes.SequenceEqual(data.Proof))
|
||||
if (!JToken.DeepEquals(blob, data.Proof is null ? null : JObject.Parse(data.Proof)))
|
||||
{
|
||||
data.Proof = bytes;
|
||||
data.Proof = blob.ToString(Formatting.None);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,13 +10,13 @@ namespace BTCPayServer.Data
|
||||
|
||||
public static PullPaymentBlob GetBlob(this PullPaymentData data)
|
||||
{
|
||||
var result = JsonConvert.DeserializeObject<PullPaymentBlob>(Encoding.UTF8.GetString(data.Blob));
|
||||
var result = JsonConvert.DeserializeObject<PullPaymentBlob>(data.Blob);
|
||||
result!.SupportedPaymentMethods = result.SupportedPaymentMethods.Where(id => id is not null).ToArray();
|
||||
return result;
|
||||
}
|
||||
public static void SetBlob(this PullPaymentData data, PullPaymentBlob blob)
|
||||
{
|
||||
data.Blob = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob));
|
||||
data.Blob = JsonConvert.SerializeObject(blob).ToString();
|
||||
}
|
||||
|
||||
public static bool IsSupported(this PullPaymentData data, Payments.PaymentMethodId paymentId)
|
||||
|
@ -1,12 +1,10 @@
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
namespace BTCPayServer.Events;
|
||||
|
||||
public class UserApprovedEvent
|
||||
{
|
||||
public class UserApprovedEvent
|
||||
{
|
||||
public ApplicationUser User { get; set; }
|
||||
public bool Approved { get; set; }
|
||||
public Uri RequestUri { get; set; }
|
||||
}
|
||||
public ApplicationUser User { get; set; }
|
||||
public Uri RequestUri { get; set; }
|
||||
}
|
||||
|
10
BTCPayServer/Events/UserConfirmedEmailEvent.cs
Normal file
10
BTCPayServer/Events/UserConfirmedEmailEvent.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Events;
|
||||
|
||||
public class UserConfirmedEmailEvent
|
||||
{
|
||||
public ApplicationUser User { get; set; }
|
||||
public Uri RequestUri { get; set; }
|
||||
}
|
10
BTCPayServer/Events/UserInviteAcceptedEvent.cs
Normal file
10
BTCPayServer/Events/UserInviteAcceptedEvent.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Events;
|
||||
|
||||
public class UserInviteAcceptedEvent
|
||||
{
|
||||
public ApplicationUser User { get; set; }
|
||||
public Uri RequestUri { get; set; }
|
||||
}
|
@ -2,14 +2,20 @@ using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class UserRegisteredEvent
|
||||
{
|
||||
public ApplicationUser User { get; set; }
|
||||
public bool Admin { get; set; }
|
||||
public Uri RequestUri { get; set; }
|
||||
namespace BTCPayServer.Events;
|
||||
|
||||
public TaskCompletionSource<Uri> CallbackUrlGenerated;
|
||||
}
|
||||
public class UserRegisteredEvent
|
||||
{
|
||||
public ApplicationUser User { get; set; }
|
||||
public bool Admin { get; set; }
|
||||
public UserRegisteredEventKind Kind { get; set; } = UserRegisteredEventKind.Registration;
|
||||
public Uri RequestUri { get; set; }
|
||||
public ApplicationUser InvitedByUser { get; set; }
|
||||
public TaskCompletionSource<Uri> CallbackUrlGenerated;
|
||||
}
|
||||
|
||||
public enum UserRegisteredEventKind
|
||||
{
|
||||
Registration,
|
||||
Invite
|
||||
}
|
||||
|
@ -12,28 +12,49 @@ namespace BTCPayServer.Services
|
||||
|
||||
private static string CallToAction(string actionName, string actionLink)
|
||||
{
|
||||
string button = $"{BUTTON_HTML}".Replace("{button_description}", actionName, System.StringComparison.InvariantCulture);
|
||||
button = button.Replace("{button_link}", actionLink, System.StringComparison.InvariantCulture);
|
||||
return button;
|
||||
var button = $"{BUTTON_HTML}".Replace("{button_description}", actionName, System.StringComparison.InvariantCulture);
|
||||
return button.Replace("{button_link}", HtmlEncoder.Default.Encode(actionLink), System.StringComparison.InvariantCulture);
|
||||
}
|
||||
|
||||
private static string CreateEmailBody(string body)
|
||||
{
|
||||
return $"<html><body style='{BODY_STYLE}'>{HEADER_HTML}{body}</body></html>";
|
||||
}
|
||||
|
||||
public static void SendEmailConfirmation(this IEmailSender emailSender, MailboxAddress address, string link)
|
||||
{
|
||||
emailSender.SendEmail(address, "Confirm your email",
|
||||
$"Please confirm your account by clicking this link: <a href='{HtmlEncoder.Default.Encode(link)}'>link</a>");
|
||||
emailSender.SendEmail(address, "Confirm your email", CreateEmailBody(
|
||||
$"Please confirm your account.<br/><br/>{CallToAction("Confirm Email", link)}"));
|
||||
}
|
||||
|
||||
public static void SendApprovalConfirmation(this IEmailSender emailSender, MailboxAddress address, string link)
|
||||
{
|
||||
emailSender.SendEmail(address, "Your account has been approved",
|
||||
$"Your account has been approved and you can now <a href='{HtmlEncoder.Default.Encode(link)}'>login here</a>");
|
||||
emailSender.SendEmail(address, "Your account has been approved", CreateEmailBody(
|
||||
$"Your account has been approved and you can now <a href='{HtmlEncoder.Default.Encode(link)}'>login here</a>."));
|
||||
}
|
||||
|
||||
public static void SendSetPasswordConfirmation(this IEmailSender emailSender, MailboxAddress address, string link, bool newPassword)
|
||||
public static void SendResetPassword(this IEmailSender emailSender, MailboxAddress address, string link)
|
||||
{
|
||||
var subject = $"{(newPassword ? "Set" : "Update")} Password";
|
||||
var body = $"A request has been made to {(newPassword ? "set" : "update")} your BTCPay Server password. Please confirm your password by clicking below. <br/><br/> {CallToAction(subject, HtmlEncoder.Default.Encode(link))}";
|
||||
emailSender.SendEmail(address, subject, $"<html><body style='{BODY_STYLE}'>{HEADER_HTML}{body}</body></html>");
|
||||
emailSender.SendEmail(address, "Update Password", CreateEmailBody(
|
||||
$"A request has been made to reset your BTCPay Server password. Please set your password by clicking below.<br/><br/>{CallToAction("Update Password", link)}"));
|
||||
}
|
||||
|
||||
public static void SendInvitation(this IEmailSender emailSender, MailboxAddress address, string link)
|
||||
{
|
||||
emailSender.SendEmail(address, "Invitation", CreateEmailBody(
|
||||
$"Please complete your account setup by clicking <a href='{HtmlEncoder.Default.Encode(link)}'>this link</a>."));
|
||||
}
|
||||
|
||||
public static void SendNewUserInfo(this IEmailSender emailSender, MailboxAddress address, string newUserInfo, string link)
|
||||
{
|
||||
emailSender.SendEmail(address, newUserInfo, CreateEmailBody(
|
||||
$"{newUserInfo}. You can verify and approve the account here: <a href='{HtmlEncoder.Default.Encode(link)}'>User details</a>"));
|
||||
}
|
||||
|
||||
public static void SendUserInviteAcceptedInfo(this IEmailSender emailSender, MailboxAddress address, string userInfo, string link)
|
||||
{
|
||||
emailSender.SendEmail(address, userInfo, CreateEmailBody(
|
||||
$"{userInfo}. You can view the store users here: <a href='{HtmlEncoder.Default.Encode(link)}'>Store users</a>"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ namespace BTCPayServer
|
||||
{
|
||||
public static StoreRole? GetStoreRoleOfUser(this StoreData store, string userId)
|
||||
{
|
||||
return store.UserStores.FirstOrDefault(r => r.ApplicationUserId == userId)?.StoreRole;
|
||||
return store.UserStores?.FirstOrDefault(r => r.ApplicationUserId == userId)?.StoreRole;
|
||||
}
|
||||
|
||||
public static PermissionSet GetPermissionSet(this StoreRole storeRole, string storeId)
|
||||
|
@ -23,6 +23,25 @@ namespace Microsoft.AspNetCore.Mvc
|
||||
return null;
|
||||
}
|
||||
#nullable restore
|
||||
|
||||
public static string UserDetailsLink(this LinkGenerator urlHelper, string userId, string scheme, HostString host, string pathbase)
|
||||
{
|
||||
return urlHelper.GetUriByAction(nameof(UIServerController.User), "UIServer",
|
||||
new { userId }, scheme, host, pathbase);
|
||||
}
|
||||
|
||||
public static string StoreUsersLink(this LinkGenerator urlHelper, string storeId, string scheme, HostString host, string pathbase)
|
||||
{
|
||||
return urlHelper.GetUriByAction(nameof(UIStoresController.StoreUsers), "UIStores",
|
||||
new { storeId }, scheme, host, pathbase);
|
||||
}
|
||||
|
||||
public static string InvitationLink(this LinkGenerator urlHelper, string userId, string code, string scheme, HostString host, string pathbase)
|
||||
{
|
||||
return urlHelper.GetUriByAction(nameof(UIAccountController.AcceptInvite), "UIAccount",
|
||||
new { userId, code }, scheme, host, pathbase);
|
||||
}
|
||||
|
||||
public static string EmailConfirmationLink(this LinkGenerator urlHelper, string userId, string code, string scheme, HostString host, string pathbase)
|
||||
{
|
||||
return urlHelper.GetUriByAction(nameof(UIAccountController.ConfirmEmail), "UIAccount",
|
||||
@ -33,8 +52,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
return urlHelper.GetUriByAction(nameof(UIAccountController.Login), "UIAccount", null , scheme, host, pathbase);
|
||||
}
|
||||
|
||||
public static string ResetPasswordCallbackLink(this LinkGenerator urlHelper, string userId, string code, string scheme, HostString host, string pathbase)
|
||||
public static string ResetPasswordLink(this LinkGenerator urlHelper, string userId, string code, string scheme, HostString host, string pathbase)
|
||||
{
|
||||
return urlHelper.GetUriByAction(
|
||||
action: nameof(UIAccountController.SetPassword),
|
||||
|
@ -22,7 +22,7 @@ using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public class UIFormsController : Controller
|
||||
{
|
||||
private readonly FormDataService _formDataService;
|
||||
@ -48,6 +48,7 @@ public class UIFormsController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("~/stores/{storeId}/forms/new")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult Create(string storeId)
|
||||
{
|
||||
var vm = new ModifyForm { FormConfig = new Form().ToString() };
|
||||
@ -55,6 +56,7 @@ public class UIFormsController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("~/stores/{storeId}/forms/modify/{id}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Modify(string storeId, string id)
|
||||
{
|
||||
var form = await _formDataService.GetForm(storeId, id);
|
||||
@ -66,6 +68,7 @@ public class UIFormsController : Controller
|
||||
}
|
||||
|
||||
[HttpPost("~/stores/{storeId}/forms/modify/{id?}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Modify(string storeId, string? id, ModifyForm modifyForm)
|
||||
{
|
||||
if (id is not null)
|
||||
@ -122,6 +125,7 @@ public class UIFormsController : Controller
|
||||
}
|
||||
|
||||
[HttpPost("~/stores/{storeId}/forms/{id}/remove")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Remove(string storeId, string id)
|
||||
{
|
||||
await _formDataService.RemoveForm(id, storeId);
|
||||
|
@ -135,7 +135,7 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
(await _EmailSenderFactory.GetEmailSender(invoice.StoreId)).SendEmail(
|
||||
notificationEmail,
|
||||
$"{storeName} Invoice Notification - ${invoice.StoreId}",
|
||||
$"Invoice Notification - ${invoice.StoreId}",
|
||||
emailBody);
|
||||
}
|
||||
|
||||
|
@ -8,116 +8,167 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MimeKit;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
namespace BTCPayServer.HostedServices;
|
||||
|
||||
public class UserEventHostedService(
|
||||
EventAggregator eventAggregator,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
EmailSenderFactory emailSenderFactory,
|
||||
NotificationSender notificationSender,
|
||||
StoreRepository storeRepository,
|
||||
LinkGenerator generator,
|
||||
Logs logs)
|
||||
: EventHostedServiceBase(eventAggregator, logs)
|
||||
{
|
||||
public class UserEventHostedService : EventHostedServiceBase
|
||||
protected override void SubscribeToEvents()
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly EmailSenderFactory _emailSenderFactory;
|
||||
private readonly NotificationSender _notificationSender;
|
||||
private readonly LinkGenerator _generator;
|
||||
Subscribe<UserRegisteredEvent>();
|
||||
Subscribe<UserApprovedEvent>();
|
||||
Subscribe<UserConfirmedEmailEvent>();
|
||||
Subscribe<UserPasswordResetRequestedEvent>();
|
||||
Subscribe<UserInviteAcceptedEvent>();
|
||||
}
|
||||
|
||||
public UserEventHostedService(
|
||||
EventAggregator eventAggregator,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
EmailSenderFactory emailSenderFactory,
|
||||
NotificationSender notificationSender,
|
||||
LinkGenerator generator,
|
||||
Logs logs) : base(eventAggregator, logs)
|
||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||
{
|
||||
string code;
|
||||
string callbackUrl;
|
||||
Uri uri;
|
||||
HostString host;
|
||||
ApplicationUser user;
|
||||
IEmailSender emailSender;
|
||||
switch (evt)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_emailSenderFactory = emailSenderFactory;
|
||||
_notificationSender = notificationSender;
|
||||
_generator = generator;
|
||||
case UserRegisteredEvent ev:
|
||||
user = ev.User;
|
||||
uri = ev.RequestUri;
|
||||
host = new HostString(uri.Host, uri.Port);
|
||||
|
||||
// can be either a self-registration or by invite from another user
|
||||
var isInvite = ev.Kind == UserRegisteredEventKind.Invite;
|
||||
var type = ev.Admin ? "admin" : "user";
|
||||
var info = isInvite ? ev.InvitedByUser != null ? $"invited by {ev.InvitedByUser.Email}" : "invited" : "registered";
|
||||
var requiresApproval = user.RequiresApproval && !user.Approved;
|
||||
var requiresEmailConfirmation = user.RequiresEmailConfirmation && !user.EmailConfirmed;
|
||||
|
||||
// log registration info
|
||||
var newUserInfo = $"New {type} {user.Email} {info}";
|
||||
Logs.PayServer.LogInformation(newUserInfo);
|
||||
|
||||
// send notification if the user does not require email confirmation.
|
||||
// inform admins only about qualified users and not annoy them with bot registrations.
|
||||
if (requiresApproval && !requiresEmailConfirmation)
|
||||
{
|
||||
await NotifyAdminsAboutUserRequiringApproval(user, uri, newUserInfo);
|
||||
}
|
||||
|
||||
// set callback result and send email to user
|
||||
emailSender = await emailSenderFactory.GetEmailSender();
|
||||
if (isInvite)
|
||||
{
|
||||
code = await userManager.GenerateInvitationTokenAsync(user);
|
||||
callbackUrl = generator.InvitationLink(user.Id, code, uri.Scheme, host, uri.PathAndQuery);
|
||||
ev.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
||||
|
||||
emailSender.SendInvitation(user.GetMailboxAddress(), callbackUrl);
|
||||
}
|
||||
else if (requiresEmailConfirmation)
|
||||
{
|
||||
code = await userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
callbackUrl = generator.EmailConfirmationLink(user.Id, code, uri.Scheme, host, uri.PathAndQuery);
|
||||
ev.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
||||
|
||||
emailSender.SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
ev.CallbackUrlGenerated?.SetResult(null);
|
||||
}
|
||||
break;
|
||||
|
||||
case UserPasswordResetRequestedEvent pwResetEvent:
|
||||
user = pwResetEvent.User;
|
||||
uri = pwResetEvent.RequestUri;
|
||||
host = new HostString(uri.Host, uri.Port);
|
||||
code = await userManager.GeneratePasswordResetTokenAsync(user);
|
||||
callbackUrl = generator.ResetPasswordLink(user.Id, code, uri.Scheme, host, uri.PathAndQuery);
|
||||
pwResetEvent.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
||||
Logs.PayServer.LogInformation("User {Email} requested a password reset", user.Email);
|
||||
emailSender = await emailSenderFactory.GetEmailSender();
|
||||
emailSender.SendResetPassword(user.GetMailboxAddress(), callbackUrl);
|
||||
break;
|
||||
|
||||
case UserApprovedEvent approvedEvent:
|
||||
user = approvedEvent.User;
|
||||
if (!user.Approved) break;
|
||||
uri = approvedEvent.RequestUri;
|
||||
host = new HostString(uri.Host, uri.Port);
|
||||
callbackUrl = generator.LoginLink(uri.Scheme, host, uri.PathAndQuery);
|
||||
emailSender = await emailSenderFactory.GetEmailSender();
|
||||
emailSender.SendApprovalConfirmation(user.GetMailboxAddress(), callbackUrl);
|
||||
break;
|
||||
|
||||
case UserConfirmedEmailEvent confirmedEvent:
|
||||
user = confirmedEvent.User;
|
||||
if (!user.EmailConfirmed) break;
|
||||
uri = confirmedEvent.RequestUri;
|
||||
var confirmedUserInfo = $"User {user.Email} confirmed their email address";
|
||||
Logs.PayServer.LogInformation(confirmedUserInfo);
|
||||
if (!user.RequiresApproval || user.Approved) return;
|
||||
await NotifyAdminsAboutUserRequiringApproval(user, uri, confirmedUserInfo);
|
||||
break;
|
||||
|
||||
case UserInviteAcceptedEvent inviteAcceptedEvent:
|
||||
user = inviteAcceptedEvent.User;
|
||||
uri = inviteAcceptedEvent.RequestUri;
|
||||
Logs.PayServer.LogInformation("User {Email} accepted the invite", user.Email);
|
||||
await NotifyAboutUserAcceptingInvite(user, uri);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void SubscribeToEvents()
|
||||
private async Task NotifyAdminsAboutUserRequiringApproval(ApplicationUser user, Uri uri, string newUserInfo)
|
||||
{
|
||||
if (!user.RequiresApproval || user.Approved) return;
|
||||
// notification
|
||||
await notificationSender.SendNotification(new AdminScope(), new NewUserRequiresApprovalNotification(user));
|
||||
// email
|
||||
var admins = await userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
var host = new HostString(uri.Host, uri.Port);
|
||||
var approvalLink = generator.UserDetailsLink(user.Id, uri.Scheme, host, uri.PathAndQuery);
|
||||
var emailSender = await emailSenderFactory.GetEmailSender();
|
||||
foreach (var admin in admins)
|
||||
{
|
||||
Subscribe<UserRegisteredEvent>();
|
||||
Subscribe<UserApprovedEvent>();
|
||||
Subscribe<UserPasswordResetRequestedEvent>();
|
||||
emailSender.SendNewUserInfo(admin.GetMailboxAddress(), newUserInfo, approvalLink);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||
private async Task NotifyAboutUserAcceptingInvite(ApplicationUser user, Uri uri)
|
||||
{
|
||||
var stores = await storeRepository.GetStoresByUserId(user.Id);
|
||||
var notifyRoles = new[] { StoreRoleId.Owner, StoreRoleId.Manager };
|
||||
foreach (var store in stores)
|
||||
{
|
||||
string code;
|
||||
string callbackUrl;
|
||||
Uri uri;
|
||||
HostString host;
|
||||
ApplicationUser user;
|
||||
MailboxAddress address;
|
||||
IEmailSender emailSender;
|
||||
UserPasswordResetRequestedEvent userPasswordResetRequestedEvent;
|
||||
switch (evt)
|
||||
// notification
|
||||
await notificationSender.SendNotification(new StoreScope(store.Id, notifyRoles), new InviteAcceptedNotification(user, store));
|
||||
// email
|
||||
var notifyUsers = await storeRepository.GetStoreUsers(store.Id, notifyRoles);
|
||||
var host = new HostString(uri.Host, uri.Port);
|
||||
var storeUsersLink = generator.StoreUsersLink(store.Id, uri.Scheme, host, uri.PathAndQuery);
|
||||
var emailSender = await emailSenderFactory.GetEmailSender(store.Id);
|
||||
foreach (var storeUser in notifyUsers)
|
||||
{
|
||||
case UserRegisteredEvent userRegisteredEvent:
|
||||
user = userRegisteredEvent.User;
|
||||
Logs.PayServer.LogInformation(
|
||||
$"A new user just registered {user.Email} {(userRegisteredEvent.Admin ? "(admin)" : "")}");
|
||||
if (user.RequiresApproval && !user.Approved)
|
||||
{
|
||||
await _notificationSender.SendNotification(new AdminScope(), new NewUserRequiresApprovalNotification(user));
|
||||
}
|
||||
if (!user.EmailConfirmed && user.RequiresEmailConfirmation)
|
||||
{
|
||||
uri = userRegisteredEvent.RequestUri;
|
||||
host = new HostString(uri.Host, uri.Port);
|
||||
code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
callbackUrl = _generator.EmailConfirmationLink(user.Id, code, uri.Scheme, host, uri.PathAndQuery);
|
||||
userRegisteredEvent.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
||||
address = user.GetMailboxAddress();
|
||||
emailSender = await _emailSenderFactory.GetEmailSender();
|
||||
emailSender.SendEmailConfirmation(address, callbackUrl);
|
||||
}
|
||||
else if (!await _userManager.HasPasswordAsync(userRegisteredEvent.User))
|
||||
{
|
||||
userPasswordResetRequestedEvent = new UserPasswordResetRequestedEvent
|
||||
{
|
||||
CallbackUrlGenerated = userRegisteredEvent.CallbackUrlGenerated,
|
||||
User = user,
|
||||
RequestUri = userRegisteredEvent.RequestUri
|
||||
};
|
||||
goto passwordSetter;
|
||||
}
|
||||
else
|
||||
{
|
||||
userRegisteredEvent.CallbackUrlGenerated?.SetResult(null);
|
||||
}
|
||||
break;
|
||||
|
||||
case UserApprovedEvent userApprovedEvent:
|
||||
if (userApprovedEvent.Approved)
|
||||
{
|
||||
uri = userApprovedEvent.RequestUri;
|
||||
host = new HostString(uri.Host, uri.Port);
|
||||
address = userApprovedEvent.User.GetMailboxAddress();
|
||||
callbackUrl = _generator.LoginLink(uri.Scheme, host, uri.PathAndQuery);
|
||||
emailSender = await _emailSenderFactory.GetEmailSender();
|
||||
emailSender.SendApprovalConfirmation(address, callbackUrl);
|
||||
}
|
||||
break;
|
||||
|
||||
case UserPasswordResetRequestedEvent userPasswordResetRequestedEvent2:
|
||||
userPasswordResetRequestedEvent = userPasswordResetRequestedEvent2;
|
||||
passwordSetter:
|
||||
uri = userPasswordResetRequestedEvent.RequestUri;
|
||||
host = new HostString(uri.Host, uri.Port);
|
||||
user = userPasswordResetRequestedEvent.User;
|
||||
code = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||
var newPassword = await _userManager.HasPasswordAsync(user);
|
||||
callbackUrl = _generator.ResetPasswordCallbackLink(user.Id, code, uri.Scheme, host, uri.PathAndQuery);
|
||||
userPasswordResetRequestedEvent.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
||||
address = user.GetMailboxAddress();
|
||||
emailSender = await _emailSenderFactory.GetEmailSender();
|
||||
emailSender.SendSetPasswordConfirmation(address, callbackUrl, newPassword);
|
||||
break;
|
||||
if (storeUser.Id == user.Id) continue; // do not notify the user itself (if they were added as owner or manager)
|
||||
var notifyUser = await userManager.FindByIdOrEmail(storeUser.Id);
|
||||
var info = $"User {user.Email} accepted the invite to {store.StoreName}";
|
||||
emailSender.SendUserInviteAcceptedInfo(notifyUser.GetMailboxAddress(), info, storeUsersLink);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,8 @@
|
||||
using System;
|
||||
using System.Configuration.Provider;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Custodians;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Abstractions.Services;
|
||||
@ -66,11 +63,9 @@ using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
using NBitpayClient;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
using NicolasDorier.RateLimits;
|
||||
using Serilog;
|
||||
using BTCPayServer.Services.Reporting;
|
||||
using BTCPayServer.Services.WalletFileParsing;
|
||||
@ -437,6 +432,7 @@ namespace BTCPayServer.Hosting
|
||||
|
||||
services.AddSingleton<INotificationHandler, NewVersionNotification.Handler>();
|
||||
services.AddSingleton<INotificationHandler, NewUserRequiresApprovalNotification.Handler>();
|
||||
services.AddSingleton<INotificationHandler, InviteAcceptedNotification.Handler>();
|
||||
services.AddSingleton<INotificationHandler, PluginUpdateNotification.Handler>();
|
||||
services.AddSingleton<INotificationHandler, InvoiceEventNotification.Handler>();
|
||||
services.AddSingleton<INotificationHandler, PayoutNotification.Handler>();
|
||||
|
@ -88,7 +88,8 @@ namespace BTCPayServer.Hosting
|
||||
.PersistKeysToFileSystem(new DirectoryInfo(new DataDirectories().Configure(Configuration).DataDir));
|
||||
services.AddIdentity<ApplicationUser, IdentityRole>()
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
.AddDefaultTokenProviders()
|
||||
.AddInvitationTokenProvider();
|
||||
services.Configure<AuthenticationOptions>(opts =>
|
||||
{
|
||||
opts.DefaultAuthenticateScheme = null;
|
||||
|
@ -19,5 +19,6 @@ namespace BTCPayServer.Models.AccountViewModels
|
||||
|
||||
public string Code { get; set; }
|
||||
public bool EmailSetInternally { get; set; }
|
||||
public bool HasPassword { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +71,9 @@ namespace BTCPayServer.Models
|
||||
|
||||
public PaymentMethodId[] PaymentMethods { get; set; }
|
||||
|
||||
public string SetupDeepLink { get; set; }
|
||||
public string ResetDeepLink { get; set; }
|
||||
|
||||
public string HubPath { get; set; }
|
||||
public string ResetIn { get; set; }
|
||||
public string Email { get; set; }
|
||||
|
@ -19,6 +19,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
public IEnumerable<PaymentMethodId> PaymentMethods { get; set; }
|
||||
public PayoutState PayoutState { get; set; }
|
||||
public string PullPaymentName { get; set; }
|
||||
public bool HasPayoutProcessor { get; set; }
|
||||
|
||||
public class PayoutModel
|
||||
{
|
||||
|
@ -132,7 +132,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
// LNDhub-compatible implementations might not offer all of GetInfo data.
|
||||
// Skip checks in those cases, see https://github.com/lnbits/lnbits/issues/1182
|
||||
var isLndHub = client is LndHubLightningClient;
|
||||
|
||||
|
||||
LightningNodeInformation info;
|
||||
try
|
||||
{
|
||||
@ -163,11 +163,14 @@ namespace BTCPayServer.Payments.Lightning
|
||||
? info.NodeInfoList.Where(i => i.IsTor == preferOnion.Value).ToArray()
|
||||
: info.NodeInfoList.Select(i => i).ToArray();
|
||||
|
||||
var blocksGap = summary.Status.ChainHeight - info.BlockHeight;
|
||||
if (blocksGap > 10 && !(isLndHub && info.BlockHeight == 0))
|
||||
if (summary.Status is not null)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException(
|
||||
$"The lightning node is not synched ({blocksGap} blocks left)");
|
||||
var blocksGap = summary.Status.ChainHeight - info.BlockHeight;
|
||||
if (blocksGap > 10 && !(isLndHub && info.BlockHeight == 0))
|
||||
{
|
||||
throw new PaymentMethodUnavailableException(
|
||||
$"The lightning node is not synched ({blocksGap} blocks left)");
|
||||
}
|
||||
}
|
||||
return nodeInfo;
|
||||
}
|
||||
|
@ -508,8 +508,15 @@ namespace BTCPayServer.Payments.Lightning
|
||||
try
|
||||
{
|
||||
var lightningClient = _lightningClientFactory.Create(ConnectionString, _network);
|
||||
if(lightningClient is null)
|
||||
return;
|
||||
uri = lightningClient.GetServerUri();
|
||||
logUrl = string.IsNullOrEmpty(uri.UserInfo) ? uri.ToString() : uri.ToString().Replace(uri.UserInfo, "***");
|
||||
logUrl = uri switch
|
||||
{
|
||||
null when LightningConnectionStringHelper.ExtractValues(ConnectionString, out var type) is not null => type,
|
||||
null => string.Empty,
|
||||
_ => string.IsNullOrEmpty(uri.UserInfo) ? uri.ToString() : uri.ToString().Replace(uri.UserInfo, "***")
|
||||
};
|
||||
Logs.PayServer.LogInformation("{CryptoCode} (Lightning): Start listening {Uri}", _network.CryptoCode, logUrl);
|
||||
using var session = await lightningClient.Listen(cancellation);
|
||||
// Just in case the payment arrived after our last poll but before we listened.
|
||||
|
@ -99,17 +99,13 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
|
||||
{
|
||||
Logs.PayServer.LogError(e, $"Could not process payout {payoutData.Id}");
|
||||
}
|
||||
|
||||
|
||||
if (payoutData.State != PayoutState.InProgress || payoutData.Proof is not null)
|
||||
{
|
||||
var result = await _pullPaymentHostedService.MarkPaid(new MarkPayoutRequest()
|
||||
await _pullPaymentHostedService.MarkPaid(new MarkPayoutRequest()
|
||||
{
|
||||
State = payoutData.State,
|
||||
PayoutId = payoutData.Id,
|
||||
Proof = payoutData.GetProofBlobJson()
|
||||
State = payoutData.State, PayoutId = payoutData.Id, Proof = payoutData.GetProofBlobJson()
|
||||
});
|
||||
if(result != MarkPayoutRequest.PayoutPaidResult.Ok)
|
||||
Logs.PayServer.LogError($"Could not mark payout {payoutData.Id} as {payoutData.State} because {result}");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,9 +31,10 @@ public class UILightningAutomatedPayoutProcessorsController : Controller
|
||||
_lightningAutomatedPayoutSenderFactory = lightningAutomatedPayoutSenderFactory;
|
||||
_payoutProcessorService = payoutProcessorService;
|
||||
}
|
||||
|
||||
[HttpGet("~/stores/{storeId}/payout-processors/lightning-automated/{cryptocode}")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Configure(string storeId, string cryptoCode)
|
||||
{
|
||||
if (!_lightningAutomatedPayoutSenderFactory.GetSupportedPaymentMethods().Any(id =>
|
||||
|
@ -34,10 +34,9 @@ public class UIOnChainAutomatedPayoutProcessorsController : Controller
|
||||
_payoutProcessorService = payoutProcessorService;
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("~/stores/{storeId}/payout-processors/onchain-automated/{cryptocode}")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Configure(string storeId, string cryptoCode)
|
||||
{
|
||||
if (!_onChainAutomatedPayoutSenderFactory.GetSupportedPaymentMethods().Any(id =>
|
||||
|
@ -34,7 +34,7 @@ public class UIPayoutProcessorsController : Controller
|
||||
|
||||
[HttpGet("~/stores/{storeId}/payout-processors")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ConfigureStorePayoutProcessors(string storeId)
|
||||
{
|
||||
var activeProcessors =
|
||||
|
@ -3,10 +3,10 @@ using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
|
||||
{
|
||||
public partial class SyncInfoResponse
|
||||
public partial class GetInfoResponse
|
||||
{
|
||||
[JsonProperty("height")] public long Height { get; set; }
|
||||
[JsonProperty("peers")] public List<Peer> Peers { get; set; }
|
||||
[JsonProperty("busy_syncing")] public bool BusySyncing { get; set; }
|
||||
[JsonProperty("status")] public string Status { get; set; }
|
||||
[JsonProperty("target_height")] public long? TargetHeight { get; set; }
|
||||
}
|
@ -132,26 +132,9 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
}
|
||||
|
||||
JObject formResponseJObject = null;
|
||||
|
||||
if (settings.FormId is not null)
|
||||
{
|
||||
var formData = await FormDataService.GetForm(settings.FormId);
|
||||
if (formData is not null)
|
||||
{
|
||||
formResponseJObject = TryParseJObject(formResponse) ?? new JObject();
|
||||
var form = Form.Parse(formData.Config);
|
||||
FormDataService.SetValues(form, formResponseJObject);
|
||||
if (!FormDataService.Validate(form, ModelState))
|
||||
{
|
||||
// someone tried to bypass validation
|
||||
return RedirectToAction(nameof(ViewCrowdfund), new { appId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var store = await _appService.GetStore(app);
|
||||
var title = settings.Title;
|
||||
decimal? price = request.Amount;
|
||||
var title = settings.Title;
|
||||
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
|
||||
ViewPointOfSaleViewModel.Item choice = null;
|
||||
if (!string.IsNullOrEmpty(request.ChoiceKey))
|
||||
@ -194,6 +177,48 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
|
||||
price = request.Amount;
|
||||
}
|
||||
|
||||
if (settings.FormId is not null)
|
||||
{
|
||||
var formData = await FormDataService.GetForm(settings.FormId);
|
||||
if (formData is not null)
|
||||
{
|
||||
formResponseJObject = TryParseJObject(formResponse) ?? new JObject();
|
||||
var form = Form.Parse(formData.Config);
|
||||
FormDataService.SetValues(form, formResponseJObject);
|
||||
if (!FormDataService.Validate(form, ModelState))
|
||||
{
|
||||
// someone tried to bypass validation
|
||||
return RedirectToAction(nameof(ViewCrowdfund), new { appId });
|
||||
}
|
||||
|
||||
|
||||
var amtField = form.GetFieldByFullName($"{FormDataService.InvoiceParameterPrefix}amount");
|
||||
if (amtField is null)
|
||||
{
|
||||
form.Fields.Add(new Field
|
||||
{
|
||||
Name = $"{FormDataService.InvoiceParameterPrefix}amount",
|
||||
Type = "hidden",
|
||||
Value = price?.ToString(),
|
||||
Constant = true
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
amtField.Value = price?.ToString();
|
||||
}
|
||||
formResponseJObject = FormDataService.GetValues(form);
|
||||
|
||||
var invoiceRequest = FormDataService.GenerateInvoiceParametersFromForm(form);
|
||||
if (invoiceRequest.Amount is not null)
|
||||
{
|
||||
price = invoiceRequest.Amount.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price >
|
||||
(info.TargetAmount - (info.Info.CurrentAmount + info.Info.CurrentPendingAmount))))
|
||||
@ -354,7 +379,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
return View("Views/UIForms/View", viewModel);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpGet("{appId}/settings/crowdfund")]
|
||||
public async Task<IActionResult> UpdateCrowdfund(string appId)
|
||||
{
|
||||
|
@ -236,7 +236,7 @@ namespace BTCPayServer.Plugins.Crowdfund
|
||||
|
||||
public override Task SetDefaultSettings(AppData appData, string defaultCurrency)
|
||||
{
|
||||
var emptyCrowdfund = new CrowdfundSettings { TargetCurrency = defaultCurrency };
|
||||
var emptyCrowdfund = new CrowdfundSettings { Title = appData.Name, TargetCurrency = defaultCurrency };
|
||||
appData.SetSettings(emptyCrowdfund);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
@ -101,6 +101,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
StoreBranding = storeBranding,
|
||||
Step = step.ToString(CultureInfo.InvariantCulture),
|
||||
ViewType = (PosViewType)viewType,
|
||||
ShowItems = settings.ShowItems,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
ShowDiscount = settings.ShowDiscount,
|
||||
ShowSearch = settings.ShowSearch,
|
||||
@ -216,9 +217,11 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
title = settings.Title;
|
||||
// if cart IS enabled and we detect posdata that matches the cart system's, check inventory for the items
|
||||
price = amount;
|
||||
if (currentView == PosViewType.Cart && AppService.TryParsePosCartItems(jposData, out cartItems))
|
||||
if (AppService.TryParsePosCartItems(jposData, out cartItems))
|
||||
{
|
||||
price = 0.0m;
|
||||
price = jposData.TryGetValue("amounts", out var amounts) && amounts is JArray { Count: > 0 } amountsArray
|
||||
? amountsArray.Values<decimal>().Sum()
|
||||
: 0.0m;
|
||||
choices = AppService.Parse(settings.Template, false);
|
||||
foreach (var cartItem in cartItems)
|
||||
{
|
||||
@ -292,13 +295,13 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
}
|
||||
|
||||
var amtField = form.GetFieldByFullName($"{FormDataService.InvoiceParameterPrefix}amount");
|
||||
if (amtField is null && price.HasValue)
|
||||
if (amtField is null)
|
||||
{
|
||||
form.Fields.Add(new Field
|
||||
{
|
||||
Name = $"{FormDataService.InvoiceParameterPrefix}amount",
|
||||
Type = "hidden",
|
||||
Value = price.ToString(),
|
||||
Value = price?.ToString(),
|
||||
Constant = true
|
||||
});
|
||||
}
|
||||
@ -379,6 +382,14 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
var key = selectedChoice.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed ? ident : $"{ident} ({singlePrice})";
|
||||
cartData.Add(key, $"{cartItem.Count} x {singlePrice} = {totalPrice}");
|
||||
}
|
||||
|
||||
if (jposData.TryGetValue("amounts", out var amounts) && amounts is JArray { Count: > 0 } amountsArray)
|
||||
{
|
||||
for (var i = 0; i < amountsArray.Count; i++)
|
||||
{
|
||||
cartData.Add($"Manual entry {i+1}", _displayFormatter.Currency(amountsArray[i].ToObject<decimal>(), settings.Currency, DisplayFormatter.CurrencyFormat.Symbol));
|
||||
}
|
||||
}
|
||||
receiptData.Add("Cart", cartData);
|
||||
}
|
||||
receiptData.Add("Subtotal", _displayFormatter.Currency(appPosData.Subtotal, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol));
|
||||
@ -558,7 +569,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
return Json(recent);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpGet("{appId}/settings/pos")]
|
||||
public async Task<IActionResult> UpdatePointOfSale(string appId)
|
||||
{
|
||||
@ -580,6 +591,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
AppName = app.Name,
|
||||
Title = settings.Title,
|
||||
DefaultView = settings.DefaultView,
|
||||
ShowItems = settings.ShowItems,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
ShowDiscount = settings.ShowDiscount,
|
||||
ShowSearch = settings.ShowSearch,
|
||||
@ -670,6 +682,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
{
|
||||
Title = vm.Title,
|
||||
DefaultView = vm.DefaultView,
|
||||
ShowItems = vm.ShowItems,
|
||||
ShowCustomAmount = vm.ShowCustomAmount,
|
||||
ShowDiscount = vm.ShowDiscount,
|
||||
ShowSearch = vm.ShowSearch,
|
||||
|
@ -27,6 +27,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
|
||||
|
||||
[Display(Name = "Point of Sale Style")]
|
||||
public PosViewType DefaultView { get; set; }
|
||||
[Display(Name = "Display item selection for keypad")]
|
||||
public bool ShowItems { get; set; }
|
||||
[Display(Name = "User can input custom amount")]
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
[Display(Name = "User can input discount in %")]
|
||||
|
@ -62,6 +62,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
|
||||
public string StoreName { get; set; }
|
||||
public CurrencyInfoData CurrencyInfo { get; set; }
|
||||
public PosViewType ViewType { get; set; }
|
||||
public bool ShowItems { get; set; }
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
public bool ShowDiscount { get; set; }
|
||||
public bool ShowSearch { get; set; } = true;
|
||||
|
@ -117,7 +117,7 @@ namespace BTCPayServer.Plugins.PointOfSale
|
||||
|
||||
public override Task SetDefaultSettings(AppData appData, string defaultCurrency)
|
||||
{
|
||||
var empty = new PointOfSaleSettings { Currency = defaultCurrency };
|
||||
var empty = new PointOfSaleSettings { Title = appData.Name, Currency = defaultCurrency };
|
||||
appData.SetSettings(empty);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
@ -42,7 +42,10 @@ namespace BTCPayServer.Security
|
||||
_pluginHookService = pluginHookService;
|
||||
_paymentRequestRepository = paymentRequestRepository;
|
||||
}
|
||||
|
||||
//TODO: In the future, we will add these store permissions to actual aspnet roles, and remove this class.
|
||||
private static readonly PermissionSet ServerAdminRolePermissions =
|
||||
new PermissionSet(new[] {Permission.Create(Policies.CanViewStoreSettings, null)});
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement)
|
||||
{
|
||||
if (context.User.Identity.AuthenticationType != AuthenticationSchemes.Cookie)
|
||||
@ -67,7 +70,10 @@ namespace BTCPayServer.Security
|
||||
storeId = s;
|
||||
}
|
||||
else
|
||||
{
|
||||
storeId = _httpContext.GetImplicitStoreId();
|
||||
store = _httpContext.GetStoreData();
|
||||
}
|
||||
var routeData = _httpContext.GetRouteData();
|
||||
if (routeData != null)
|
||||
{
|
||||
@ -124,12 +130,9 @@ namespace BTCPayServer.Security
|
||||
storeId = null;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(storeId))
|
||||
if (!string.IsNullOrEmpty(storeId) && store is null)
|
||||
{
|
||||
var cachedStore = _httpContext.GetStoreData();
|
||||
store = cachedStore?.Id == storeId
|
||||
? cachedStore
|
||||
: await _storeRepository.FindStore(storeId, userId);
|
||||
store = await _storeRepository.FindStore(storeId, userId);
|
||||
}
|
||||
|
||||
if (Policies.IsServerPolicy(policy) && isAdmin)
|
||||
@ -142,14 +145,18 @@ namespace BTCPayServer.Security
|
||||
}
|
||||
else if (Policies.IsStorePolicy(policy))
|
||||
{
|
||||
if (store is not null)
|
||||
if (isAdmin && storeId is not null)
|
||||
{
|
||||
if (store.HasPermission(userId,policy))
|
||||
{
|
||||
success = true;
|
||||
}
|
||||
success = ServerAdminRolePermissions.HasPermission(policy, storeId);
|
||||
|
||||
}
|
||||
else if (requiredUnscoped)
|
||||
|
||||
if (!success && store?.HasPermission(userId, policy) is true)
|
||||
{
|
||||
success = true;
|
||||
}
|
||||
|
||||
if (!success && store is null && requiredUnscoped)
|
||||
{
|
||||
success = true;
|
||||
}
|
||||
@ -166,6 +173,11 @@ namespace BTCPayServer.Security
|
||||
context.Succeed(requirement);
|
||||
if (!explicitResource)
|
||||
{
|
||||
if (storeId is not null && store is null)
|
||||
{
|
||||
store = await _storeRepository.FindStore(storeId);
|
||||
|
||||
}
|
||||
if (store != null)
|
||||
{
|
||||
if (_httpContext.GetStoreData()?.Id != store.Id)
|
||||
|
13
BTCPayServer/Security/IdentityBuilderExtension.cs
Normal file
13
BTCPayServer/Security/IdentityBuilderExtension.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace BTCPayServer.Security;
|
||||
|
||||
public static class IdentityBuilderExtension
|
||||
{
|
||||
public static IdentityBuilder AddInvitationTokenProvider(this IdentityBuilder builder)
|
||||
{
|
||||
var provider = typeof(InvitationTokenProvider<>).MakeGenericType(typeof(ApplicationUser));
|
||||
return builder.AddTokenProvider(InvitationTokenProviderOptions.ProviderName, provider);
|
||||
}
|
||||
}
|
26
BTCPayServer/Security/InvitationTokenProvider.cs
Normal file
26
BTCPayServer/Security/InvitationTokenProvider.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BTCPayServer.Security;
|
||||
|
||||
// https://andrewlock.net/implementing-custom-token-providers-for-passwordless-authentication-in-asp-net-core-identity/
|
||||
public class InvitationTokenProviderOptions : DataProtectionTokenProviderOptions
|
||||
{
|
||||
public const string ProviderName = "InvitationTokenProvider";
|
||||
|
||||
public InvitationTokenProviderOptions()
|
||||
{
|
||||
Name = ProviderName;
|
||||
TokenLifespan = TimeSpan.FromDays(7);
|
||||
}
|
||||
}
|
||||
|
||||
public class InvitationTokenProvider<TUser>(
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
IOptions<InvitationTokenProviderOptions> options,
|
||||
ILogger<DataProtectorTokenProvider<TUser>> logger)
|
||||
: DataProtectorTokenProvider<TUser>(dataProtectionProvider, options, logger)
|
||||
where TUser : class;
|
@ -73,7 +73,7 @@ namespace BTCPayServer.Services.Altcoins.Monero
|
||||
var daemonPassword =
|
||||
configuration.GetOrDefault<string>(
|
||||
$"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_daemon_password", null);
|
||||
if (daemonUri == null || walletDaemonUri == null)
|
||||
if (daemonUri == null || walletDaemonUri == null || walletDaemonWalletDirectory == null)
|
||||
{
|
||||
throw new ConfigException($"{moneroLikeSpecificBtcPayNetwork.CryptoCode} is misconfigured");
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
|
||||
public long AddressIndex { get; set; }
|
||||
public string DepositAddress { get; set; }
|
||||
public decimal NextNetworkFee { get; set; }
|
||||
public long? InvoiceSettledConfirmationThreshold { get; set; }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
@ -16,6 +16,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
|
||||
public long BlockHeight { get; set; }
|
||||
public long ConfirmationCount { get; set; }
|
||||
public string TransactionId { get; set; }
|
||||
public long? InvoiceSettledConfirmationThreshold { get; set; }
|
||||
|
||||
public BTCPayNetworkBase Network { get; set; }
|
||||
public long LockTime { get; set; } = 0;
|
||||
@ -48,6 +49,12 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (InvoiceSettledConfirmationThreshold.HasValue)
|
||||
{
|
||||
return ConfirmationCount >= InvoiceSettledConfirmationThreshold;
|
||||
}
|
||||
|
||||
switch (speedPolicy)
|
||||
{
|
||||
case SpeedPolicy.HighSpeed:
|
||||
|
@ -59,6 +59,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
|
||||
AccountIndex = supportedPaymentMethod.AccountIndex,
|
||||
AddressIndex = address.AddressIndex,
|
||||
DepositAddress = address.Address,
|
||||
InvoiceSettledConfirmationThreshold = supportedPaymentMethod.InvoiceSettledConfirmationThreshold,
|
||||
Activated = true
|
||||
};
|
||||
|
||||
|
@ -9,6 +9,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
|
||||
|
||||
public string CryptoCode { get; set; }
|
||||
public long AccountIndex { get; set; }
|
||||
public long? InvoiceSettledConfirmationThreshold { get; set; }
|
||||
[JsonIgnore]
|
||||
public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, MoneroPaymentType.Instance);
|
||||
}
|
||||
|
@ -324,6 +324,11 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
|
||||
string txId, long confirmations, long blockHeight, long locktime, InvoiceEntity invoice,
|
||||
BlockingCollection<(PaymentEntity Payment, InvoiceEntity invoice)> paymentsToUpdate)
|
||||
{
|
||||
var network = _networkProvider.GetNetwork(cryptoCode);
|
||||
var moneroPaymentMethodDetails = invoice
|
||||
.GetPaymentMethod(network, MoneroPaymentType.Instance)
|
||||
.GetPaymentMethodDetails() as MoneroLikeOnChainPaymentMethodDetails;
|
||||
|
||||
//construct the payment data
|
||||
var paymentData = new MoneroLikePaymentData()
|
||||
{
|
||||
@ -335,7 +340,8 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
|
||||
Amount = totalAmount,
|
||||
BlockHeight = blockHeight,
|
||||
Network = _networkProvider.GetNetwork(cryptoCode),
|
||||
LockTime = locktime
|
||||
LockTime = locktime,
|
||||
InvoiceSettledConfirmationThreshold = moneroPaymentMethodDetails.InvoiceSettledConfirmationThreshold
|
||||
};
|
||||
|
||||
//check if this tx exists as a payment to this invoice already
|
||||
|
@ -59,12 +59,12 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
|
||||
try
|
||||
{
|
||||
var daemonResult =
|
||||
await daemonRpcClient.SendCommandAsync<JsonRpcClient.NoRequestModel, SyncInfoResponse>("sync_info",
|
||||
await daemonRpcClient.SendCommandAsync<JsonRpcClient.NoRequestModel, GetInfoResponse>("get_info",
|
||||
JsonRpcClient.NoRequestModel.Instance);
|
||||
summary.TargetHeight = daemonResult.TargetHeight.GetValueOrDefault(0);
|
||||
summary.CurrentHeight = daemonResult.Height;
|
||||
summary.TargetHeight = summary.TargetHeight == 0 ? summary.CurrentHeight : summary.TargetHeight;
|
||||
summary.Synced = daemonResult.Height >= summary.TargetHeight && summary.CurrentHeight > 0;
|
||||
summary.Synced = !daemonResult.BusySyncing;
|
||||
summary.UpdatedAt = DateTime.UtcNow;
|
||||
summary.DaemonAvailable = true;
|
||||
}
|
||||
|
@ -104,6 +104,19 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI
|
||||
new SelectListItem(
|
||||
$"{account.AccountIndex} - {(string.IsNullOrEmpty(account.Label) ? "No label" : account.Label)}",
|
||||
account.AccountIndex.ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
var settlementThresholdChoice = MoneroLikeSettlementThresholdChoice.StoreSpeedPolicy;
|
||||
if (settings != null && settings.InvoiceSettledConfirmationThreshold is { } confirmations)
|
||||
{
|
||||
settlementThresholdChoice = confirmations switch
|
||||
{
|
||||
0 => MoneroLikeSettlementThresholdChoice.ZeroConfirmation,
|
||||
1 => MoneroLikeSettlementThresholdChoice.AtLeastOne,
|
||||
10 => MoneroLikeSettlementThresholdChoice.AtLeastTen,
|
||||
_ => MoneroLikeSettlementThresholdChoice.Custom
|
||||
};
|
||||
}
|
||||
|
||||
return new MoneroLikePaymentMethodViewModel()
|
||||
{
|
||||
WalletFileFound = System.IO.File.Exists(fileAddress),
|
||||
@ -114,7 +127,13 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI
|
||||
CryptoCode = cryptoCode,
|
||||
AccountIndex = settings?.AccountIndex ?? accountsResponse?.SubaddressAccounts?.FirstOrDefault()?.AccountIndex ?? 0,
|
||||
Accounts = accounts == null ? null : new SelectList(accounts, nameof(SelectListItem.Value),
|
||||
nameof(SelectListItem.Text))
|
||||
nameof(SelectListItem.Text)),
|
||||
SettlementConfirmationThresholdChoice = settlementThresholdChoice,
|
||||
CustomSettlementConfirmationThreshold =
|
||||
settings != null &&
|
||||
settlementThresholdChoice is MoneroLikeSettlementThresholdChoice.Custom
|
||||
? settings.InvoiceSettledConfirmationThreshold
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
@ -250,6 +269,8 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI
|
||||
vm.Enabled = viewModel.Enabled;
|
||||
vm.NewAccountLabel = viewModel.NewAccountLabel;
|
||||
vm.AccountIndex = viewModel.AccountIndex;
|
||||
vm.SettlementConfirmationThresholdChoice = viewModel.SettlementConfirmationThresholdChoice;
|
||||
vm.CustomSettlementConfirmationThreshold = viewModel.CustomSettlementConfirmationThreshold;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
@ -258,7 +279,15 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI
|
||||
storeData.SetSupportedPaymentMethod(new MoneroSupportedPaymentMethod()
|
||||
{
|
||||
AccountIndex = viewModel.AccountIndex,
|
||||
CryptoCode = viewModel.CryptoCode
|
||||
CryptoCode = viewModel.CryptoCode,
|
||||
InvoiceSettledConfirmationThreshold = viewModel.SettlementConfirmationThresholdChoice switch
|
||||
{
|
||||
MoneroLikeSettlementThresholdChoice.ZeroConfirmation => 0,
|
||||
MoneroLikeSettlementThresholdChoice.AtLeastOne => 1,
|
||||
MoneroLikeSettlementThresholdChoice.AtLeastTen => 10,
|
||||
MoneroLikeSettlementThresholdChoice.Custom when viewModel.CustomSettlementConfirmationThreshold is { } custom => custom,
|
||||
_ => null
|
||||
}
|
||||
});
|
||||
|
||||
blob.SetExcluded(new PaymentMethodId(viewModel.CryptoCode, MoneroPaymentType.Instance), !viewModel.Enabled);
|
||||
@ -297,7 +326,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI
|
||||
public IEnumerable<MoneroLikePaymentMethodViewModel> Items { get; set; }
|
||||
}
|
||||
|
||||
public class MoneroLikePaymentMethodViewModel
|
||||
public class MoneroLikePaymentMethodViewModel : IValidatableObject
|
||||
{
|
||||
public MoneroRPCProvider.MoneroLikeSummary Summary { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
@ -309,8 +338,39 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI
|
||||
public bool WalletFileFound { get; set; }
|
||||
[Display(Name = "View-Only Wallet File")]
|
||||
public IFormFile WalletFile { get; set; }
|
||||
[Display(Name = "Wallet Keys File")]
|
||||
public IFormFile WalletKeysFile { get; set; }
|
||||
[Display(Name = "Wallet Password")]
|
||||
public string WalletPassword { get; set; }
|
||||
[Display(Name = "Consider the invoice settled when the payment transaction …")]
|
||||
public MoneroLikeSettlementThresholdChoice SettlementConfirmationThresholdChoice { get; set; }
|
||||
[Display(Name = "Required Confirmations"), Range(0, 100)]
|
||||
public long? CustomSettlementConfirmationThreshold { get; set; }
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (SettlementConfirmationThresholdChoice is MoneroLikeSettlementThresholdChoice.Custom
|
||||
&& CustomSettlementConfirmationThreshold is null)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
"You must specify the number of required confirmations when using a custom threshold.",
|
||||
new[] { nameof(CustomSettlementConfirmationThreshold) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum MoneroLikeSettlementThresholdChoice
|
||||
{
|
||||
[Display(Name = "Store Speed Policy", Description = "Use the store's speed policy")]
|
||||
StoreSpeedPolicy,
|
||||
[Display(Name = "Zero Confirmation", Description = "Is unconfirmed")]
|
||||
ZeroConfirmation,
|
||||
[Display(Name = "At Least One", Description = "Has at least 1 confirmation")]
|
||||
AtLeastOne,
|
||||
[Display(Name = "At Least Ten", Description = "Has at least 10 confirmations")]
|
||||
AtLeastTen,
|
||||
[Display(Name = "Custom", Description = "Custom")]
|
||||
Custom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -87,6 +87,7 @@ namespace BTCPayServer.Services.Apps
|
||||
public string Template { get; set; }
|
||||
public bool EnableShoppingCart { get; set; }
|
||||
public PosViewType DefaultView { get; set; }
|
||||
public bool ShowItems { get; set; }
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
public bool ShowDiscount { get; set; }
|
||||
public bool ShowSearch { get; set; } = true;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user