Compare commits

..

1 Commits

Author SHA1 Message Date
4b135fdbe2 refund report 2024-02-23 13:35:09 +01:00
267 changed files with 2701 additions and 5874 deletions

2
.vscode/launch.json vendored
View File

@ -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/net8.0/BTCPayServer.dll",
"program": "${workspaceFolder}/BTCPayServer/bin/Debug/net6.0/BTCPayServer.dll",
"args": [],
"cwd": "${workspaceFolder}/BTCPayServer",
"stopAtEntry": false,

View File

@ -32,9 +32,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlSanitizer" Version="8.0.838" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.5" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0-beta.2" />
</ItemGroup>
<ItemGroup>

View File

@ -5,7 +5,6 @@ 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;
@ -86,9 +85,10 @@ namespace BTCPayServer.Abstractions.Contracts
{
o.EnableRetryOnFailure(10);
o.SetPostgresVersion(12, 0);
var mainSearchPath = GetSearchPath(_options.Value.ConnectionString);
var schemaPrefix = string.IsNullOrEmpty(_schemaPrefix) ? "__EFMigrationsHistory" : _schemaPrefix;
o.MigrationsHistoryTable(schemaPrefix, mainSearchPath);
if (!string.IsNullOrEmpty(_schemaPrefix))
{
o.MigrationsHistoryTable(_schemaPrefix);
}
})
.ReplaceService<IMigrationsSqlGenerator, CustomNpgsqlMigrationsSqlGenerator>();
break;
@ -108,11 +108,5 @@ 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];
}
}
}

View File

@ -36,17 +36,6 @@ public static class HttpRequestExtensions
request.Path.ToUriComponent());
}
public static string GetCurrentUrlWithQueryString(this HttpRequest request)
{
return string.Concat(
request.Scheme,
"://",
request.Host.ToUriComponent(),
request.PathBase.ToUriComponent(),
request.Path.ToUriComponent(),
request.QueryString.ToUriComponent());
}
public static string GetCurrentPath(this HttpRequest request)
{
return string.Concat(

View File

@ -1,35 +0,0 @@
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();
}

View File

@ -16,7 +16,7 @@
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<PropertyGroup>
<Version Condition=" '$(Version)' == '' ">1.7.4</Version>
<Version Condition=" '$(Version)' == '' ">1.7.3</Version>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@ -31,7 +31,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.5.1" />
<PackageReference Include="NBitcoin" Version="7.0.37" />
<PackageReference Include="NBitcoin" Version="7.0.34" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>

View File

@ -109,7 +109,7 @@ namespace BTCPayServer.Client
{
var response = await _httpClient.SendAsync(
CreateHttpRequest(
$"api/v1/pull-payments/{HttpUtility.UrlEncode(pullPaymentId)}/lnurl",
$"/api/v1/pull-payments/{pullPaymentId}/lnurl",
method: HttpMethod.Get), cancellationToken);
return await HandleResponse<PullPaymentLNURL>(response);
}

View File

@ -1,5 +1,4 @@
using System;
using System.Globalization;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
@ -59,8 +58,6 @@ namespace BTCPayServer.Client.JsonConverters
return null;
return TimeSpan.Zero;
}
if (reader.TokenType == JsonToken.String && TimeSpan.TryParse(reader.Value?.ToString(), CultureInfo.InvariantCulture, out var res))
return res;
if (reader.TokenType != JsonToken.Integer)
throw new JsonObjectException("Invalid timespan, expected integer", reader);
return ToTimespan((long)reader.Value);

View File

@ -26,7 +26,6 @@ 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;

View File

@ -1,5 +1,3 @@
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models;
public class EmailSettingsData
@ -28,11 +26,4 @@ public class EmailSettingsData
get; set;
}
public bool DisableCertificateCheck { get; set; }
[JsonIgnore]
public bool EnabledCertificateCheck
{
get => !DisableCertificateCheck;
set { DisableCertificateCheck = !value; }
}
}

View File

@ -1,5 +1,3 @@
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models;
public class LightningAddressData
@ -8,5 +6,5 @@ public class LightningAddressData
public string CurrencyCode { get; set; }
public decimal? Min { get; set; }
public decimal? Max { get; set; }
public JObject InvoiceMetadata { get; set; }
}

View File

@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
@ -16,15 +13,12 @@ namespace BTCPayServer.Client.Models
public bool? Archived { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset Created { get; set; }
[JsonExtensionData]
public IDictionary<string, JToken> AdditionalData { get; set; } = new Dictionary<string, JToken>();
}
}
public class PointOfSaleAppData : AppDataBase
{
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; }

View File

@ -4,7 +4,6 @@ using System.Text;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
@ -15,15 +14,11 @@ 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
{

View File

@ -4,7 +4,7 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NBXplorer.Client" Version="4.3.1" />
<PackageReference Include="NBXplorer.Client" Version="4.3.0" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
</ItemGroup>
<ItemGroup Condition="'$(Altcoins)' != 'true'">

View File

@ -107,10 +107,10 @@ namespace BTCPayServer.Data
//PayjoinLock.OnModelCreating(builder);
PaymentRequestData.OnModelCreating(builder, Database);
PaymentData.OnModelCreating(builder, Database);
PayoutData.OnModelCreating(builder, Database);
PayoutData.OnModelCreating(builder);
PendingInvoiceData.OnModelCreating(builder);
//PlannedTransaction.OnModelCreating(builder);
PullPaymentData.OnModelCreating(builder, Database);
PullPaymentData.OnModelCreating(builder);
RefundData.OnModelCreating(builder);
SettingData.OnModelCreating(builder, Database);
StoreSettingData.OnModelCreating(builder, Database);

View File

@ -3,11 +3,11 @@
<Import Project="../Build/Common.csproj" />
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />

View File

@ -1,11 +1,8 @@
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
@ -24,14 +21,14 @@ namespace BTCPayServer.Data
[MaxLength(20)]
[Required]
public string PaymentMethodId { get; set; }
public string Blob { get; set; }
public string Proof { get; set; }
public byte[] Blob { get; set; }
public byte[] Proof { get; set; }
#nullable enable
public string? Destination { get; set; }
#nullable restore
public StoreData StoreData { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
internal static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<PayoutData>()
.HasOne(o => o.PullPaymentData)
@ -46,33 +43,6 @@ 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

View File

@ -3,11 +3,8 @@ 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
@ -27,33 +24,16 @@ namespace BTCPayServer.Data
public DateTimeOffset? EndDate { get; set; }
public bool Archived { get; set; }
public List<PayoutData> Payouts { get; set; }
public string Blob { get; set; }
public byte[] Blob { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
internal static void OnModelCreating(ModelBuilder builder)
{
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)

View File

@ -1,28 +0,0 @@
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)
{
}
}
}

View File

@ -1,98 +0,0 @@
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"
})
});
}
}
}

View File

@ -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.1");
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
@ -599,7 +599,7 @@ namespace BTCPayServer.Migrations
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.HasColumnType("TEXT");
.HasColumnType("BLOB");
b.Property<DateTimeOffset>("Date")
.HasColumnType("TEXT");
@ -613,7 +613,7 @@ namespace BTCPayServer.Migrations
.HasColumnType("TEXT");
b.Property<byte[]>("Proof")
.HasColumnType("TEXT");
.HasColumnType("BLOB");
b.Property<string>("PullPaymentDataId")
.HasColumnType("TEXT");
@ -704,7 +704,7 @@ namespace BTCPayServer.Migrations
.HasColumnType("INTEGER");
b.Property<byte[]>("Blob")
.HasColumnType("TEXT");
.HasColumnType("BLOB");
b.Property<DateTimeOffset?>("EndDate")
.HasColumnType("TEXT");

View File

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
@ -110,22 +109,9 @@ namespace BTCPayServer.PluginPacker
private static Type[] GetAllExtensionTypesFromAssembly(Assembly assembly)
{
return GetLoadableTypes(assembly).Where(type =>
return assembly.GetTypes().Where(type =>
typeof(IBTCPayServerPlugin).IsAssignableFrom(type) &&
!type.IsAbstract).ToArray();
}
static Type[] GetLoadableTypes(Assembly assembly)
{
if (assembly == null)
throw new ArgumentNullException(nameof(assembly));
try
{
return assembly.GetTypes();
}
catch (ReflectionTypeLoadException e)
{
return e.Types.Where(t => t != null).ToArray();
}
}
}
}

View File

@ -4,9 +4,9 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
<PackageReference Include="NBitcoin" Version="7.0.37" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
<PackageReference Include="NBitcoin" Version="7.0.34" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.4" />
</ItemGroup>

View File

@ -9,7 +9,7 @@ namespace BTCPayServer.Services.Rates;
public class FreeCurrencyRatesRateProvider : IRateProvider
{
public RateSourceInfo RateSourceInfo => new("free-currency-rates", "Free Currency Rates", "https://currency-api.pages.dev/v1/currencies/btc.min.json");
public RateSourceInfo RateSourceInfo => new("free-currency-rates", "Free Currency Rates", "https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies/btc.min.json");
private readonly HttpClient _httpClient;
public FreeCurrencyRatesRateProvider(HttpClient httpClient)
{

View File

@ -93,7 +93,7 @@ namespace BTCPayServer.Services.Rates
if (ticker != null)
{
var pair = GetCurrencyPair(symbol);
if (pair is not null && ticker.Bid <= ticker.Ask)
if (pair is not null)
result.Add(new PairRate(pair, new BidAsk(ticker.Bid, ticker.Ask)));
}
}

View File

@ -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,7 +430,6 @@ namespace BTCPayServer.Tests
[Fact(Timeout = TestTimeout)]
[Trait("Altcoins", "Altcoins")]
[Trait("Lightning", "Lightning")]
[Trait("Integration", "Integration")]
public async Task CanUsePaymentMethodDropdown()
{
using (var s = CreateSeleniumTester())
@ -439,10 +438,10 @@ namespace BTCPayServer.Tests
s.Server.ActivateLightning();
await s.StartAsync();
s.GoToRegister();
s.RegisterNewUser(true);
s.RegisterNewUser();
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);
@ -455,25 +454,18 @@ 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("Bitcoin", currencyDropdownButton.Text);
Assert.Contains("BTC", currencyDropdownButton.Text);
currencyDropdownButton.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();
});
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();
currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies"));
Assert.Contains("Litecoin", currencyDropdownButton.Text);
Assert.Contains("LTC", currencyDropdownButton.Text);
currencyDropdownButton.Click();
TestUtils.Eventually(() =>
{
elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem"));
elements.Single(element => element.Text.Contains("Lightning")).Click();
});
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);
@ -762,7 +754,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);
@ -874,7 +866,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();

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../Build/Common.csproj" />
<PropertyGroup>
<NoWarn>$(NoWarn),xUnit1031</NoWarn>
@ -23,8 +23,8 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.15" />
<PackageReference Include="Selenium.Support" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver" Version="4.22.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="125.0.6422.14100" />
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="121.0.6167.8500" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PrivateAssets>all</PrivateAssets>

View File

@ -47,6 +47,7 @@ using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium.DevTools.V100.DOMSnapshot;
using Xunit;
using Xunit.Abstractions;
using StoreData = BTCPayServer.Data.StoreData;
@ -871,13 +872,6 @@ namespace BTCPayServer.Tests
Assert.IsType<MultisigDerivationStrategy>(((P2WSHDerivationStrategy)strategyBase).Inner);
Assert.Equal(expected, strategyBase.ToString());
foreach (var space in new[] { "\r\n", " ", "\t" })
{
var expectedWithNewLines = $"2-of-tpubDDXgATYzdQkHHhZZCMcNJj8BGDENvzMVou5v9NdxiP4rxDLj33nS233dGFW4htpVZSJ6zds9eVqAV9RyRHHiKtwQKX8eR4n4KN3Dwmj7A3h-{space}tpubDC8a54NFtQtMQAZ97VhoU9V6jVTvi9w4Y5SaAXJSBYETKg3AoX5CCKndznhPWxJUBToPCpT44s86QbKdGpKAnSjcMTGW4kE6UQ8vpBjcybW-tpubDChjnP9LXNrJp43biqjY7FH93wgRRNrNxB4Q8pH7PPRy8UPcH2S6V46WGVJ47zVGF7SyBJNCpnaogsFbsybVQckGtVhCkng3EtFn8qmxptS";
strategyBase = parser.Parse(expectedWithNewLines);
Assert.Equal(expected, strategyBase.ToString());
}
var inner = (MultisigDerivationStrategy)((P2WSHDerivationStrategy)strategyBase).Inner;
Assert.False(inner.IsLegacy);
Assert.Equal(3, inner.Keys.Count);
@ -894,7 +888,7 @@ namespace BTCPayServer.Tests
Assert.True(((DirectDerivationStrategy)strategyBase).Segwit);
// Failure cases
Assert.Throws<FormatException>(() => { parser.Parse("xpubZ661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw"); });
Assert.Throws<FormatException>(() => { parser.Parse("xpub 661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw"); }); // invalid format because of space
Assert.Throws<ParsingException>(() => { parser.ParseOutputDescriptor("invalid"); }); // invalid in general
Assert.Throws<ParsingException>(() => { parser.ParseOutputDescriptor("wpkh([8b60afd1/49h/0h/0h]xpub661MyMwAFXkMnyoBjyHndD3QwRbcGVBsTGeNZN6QGVHcfz4MPzBUxjSevweNFQx7SqmMHLdSA4FteGsRrEriu4pnVZMZWnruFFAYZATtcDw/0/*)#9x4vkw48"); }); // invalid checksum
}
@ -1353,22 +1347,17 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
filter = "status:abed, status:abed2";
search = new SearchString(filter);
Assert.Null(search.TextSearch);
Assert.Null(search.TextFilters);
Assert.Equal("", search.TextSearch);
Assert.Equal("status:abed, status:abed2", search.ToString());
Assert.Throws<KeyNotFoundException>(() => search.Filters["test"]);
Assert.Equal(2, search.Filters["status"].Count);
Assert.Equal("abed", search.Filters["status"].First());
Assert.Equal("abed2", search.Filters["status"].Skip(1).First());
filter = "StartDate:2019-04-25 01:00 AM, hekki,orderid:MYORDERID,orderid:MYORDERID_2";
filter = "StartDate:2019-04-25 01:00 AM, hekki";
search = new SearchString(filter);
Assert.Equal("2019-04-25 01:00 AM", search.Filters["startdate"].First());
Assert.Equal("hekki", search.TextSearch);
Assert.Equal("orderid:MYORDERID,orderid:MYORDERID_2", search.TextFilters);
Assert.Equal("orderid:MYORDERID,orderid:MYORDERID_2,hekki", search.TextCombined);
Assert.Equal("StartDate:2019-04-25 01:00 AM", search.WithoutSearchText());
Assert.Equal(filter, search.ToString());
// modify search
filter = $"status:settled,exceptionstatus:paidLate,unusual:true, fulltext searchterm, storeid:{storeId},startdate:2019-04-25 01:00:00";

View File

@ -13,7 +13,6 @@ 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;
@ -25,7 +24,6 @@ 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;
@ -303,16 +301,6 @@ 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 () =>
{
@ -482,16 +470,6 @@ 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 () =>
{
@ -1117,8 +1095,7 @@ namespace BTCPayServer.Tests
Assert.DoesNotContain(null, card1keys);
var card2 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
{
UID = uid,
OnExisting = OnExistingBehavior.UpdateVersion
UID = uid
});
Assert.Equal(1, card2.Version);
Assert.StartsWith("lnurlw://", card2.LNURLW);
@ -1138,35 +1115,6 @@ 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()
{
@ -2161,17 +2109,6 @@ namespace BTCPayServer.Tests
Assert.Equal("BTC", pp.Currency);
Assert.True(pp.AutoApproveClaims);
Assert.Equal(0.79m, pp.Amount);
// If an invoice doesn't have payment because it has been marked as paid, we should still be able to refund it.
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest { Amount = 5000.0m, Currency = "USD" });
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest { Status = InvoiceStatus.Settled });
var refund = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
{
PaymentMethod = method.PaymentMethod,
RefundVariant = RefundVariant.CurrentRate
});
Assert.Equal(1.0m, refund.Amount);
Assert.Equal("BTC", refund.Currency);
}
[Fact(Timeout = TestTimeout)]
@ -2350,7 +2287,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 metadata actually got saved
//also test the 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>());
@ -3469,7 +3406,6 @@ namespace BTCPayServer.Tests
var store2 = (await adminClient.CreateStore(new CreateStoreRequest() { Name = "test2" })).Id;
var address1 = Guid.NewGuid().ToString("n").Substring(0, 8);
var address2 = Guid.NewGuid().ToString("n").Substring(0, 8);
var address3 = Guid.NewGuid().ToString("n").Substring(0, 8);
Assert.Empty(await adminClient.GetStoreLightningAddresses(store.Id));
Assert.Empty(await adminClient.GetStoreLightningAddresses(store2));
@ -3497,23 +3433,13 @@ namespace BTCPayServer.Tests
await adminClient.RemoveStoreLightningAddress(store2, address2);
Assert.Empty(await adminClient.GetStoreLightningAddresses(store2));
var store3 = (await adminClient.CreateStore(new CreateStoreRequest { Name = "test3" })).Id;
Assert.Empty(await adminClient.GetStoreLightningAddresses(store3));
var metadata = JObject.FromObject(new { test = 123 });
await adminClient.AddOrUpdateStoreLightningAddress(store3, address3, new LightningAddressData
{
InvoiceMetadata = metadata
});
var lnAddresses = await adminClient.GetStoreLightningAddresses(store3);
Assert.Single(lnAddresses);
Assert.Equal(metadata, lnAddresses[0].InvoiceMetadata);
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task StoreUsersAPITest()
{
using var tester = CreateServerTester();
await tester.StartAsync();
@ -3523,83 +3449,52 @@ namespace BTCPayServer.Tests
var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings);
var roles = await client.GetServerRoles();
Assert.Equal(4, roles.Count);
Assert.Equal(2,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 manager = tester.NewAccount();
await manager.GrantAccessAsync();
var employee = tester.NewAccount();
await employee.GrantAccessAsync();
var guest = tester.NewAccount();
await guest.GrantAccessAsync();
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 managerClient = await manager.CreateClient(Policies.CanModifyStoreSettings);
var employeeClient = await employee.CreateClient(Policies.CanModifyStoreSettings);
var guestClient = await guest.CreateClient(Policies.CanModifyStoreSettings);
var user2Client = await user2.CreateClient(Policies.CanModifyStoreSettings);
//test no access to api when unrelated to store at all
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 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));
// 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 });
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = guestRole.Id, UserId = user2.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));
//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));
// updates
await client.RemoveStoreUser(user.StoreId, employee.UserId);
await AssertHttpError(403, async () => await employeeClient.GetStore(user.StoreId));
await user2Client.GetStore(user.StoreId);
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = ownerRole.Id, UserId = employee.UserId });
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 AssertAPIError("duplicate-store-user-role", async () =>
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = ownerRole.Id, UserId = employee.UserId }));
await employeeClient.RemoveStoreUser(user.StoreId, user.UserId);
await client.AddStoreUser(user.StoreId,
new StoreUserData() { Role = ownerRole.Id, UserId = user2.UserId }));
await user2Client.RemoveStoreUser(user.StoreId, user.UserId);
//test no access to api when unrelated to store at all
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.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 employeeClient.RemoveStoreUser(user.StoreId, employee.UserId));
await AssertAPIError("store-user-role-orphaned", async () => await user2Client.RemoveStoreUser(user.StoreId, user2.UserId));
}
[Fact(Timeout = 60 * 2 * 1000)]

View File

@ -90,6 +90,7 @@ 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();
@ -97,12 +98,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();
}
}
@ -407,12 +408,15 @@ namespace BTCPayServer.Tests
public void Logout()
{
if (!Driver.PageSource.Contains("id=\"Nav-Logout\"")) GoToUrl("/account");
if (!Driver.PageSource.Contains("id=\"Nav-Logout\""))
{
Driver.Navigate().GoToUrl(ServerUri);
}
Driver.FindElement(By.Id("Nav-Account")).Click();
Driver.FindElement(By.Id("Nav-Logout")).Click();
}
public void LogIn(string user, string password = "123456")
public void LogIn(string user, string password)
{
Driver.FindElement(By.Id("Email")).SendKeys(user);
Driver.FindElement(By.Id("Password")).SendKeys(password);
@ -641,38 +645,5 @@ 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);
}
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
@ -13,8 +14,10 @@ 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;
@ -24,6 +27,7 @@ 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;
@ -38,6 +42,7 @@ using OpenQA.Selenium.Support.Extensions;
using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace BTCPayServer.Tests
{
@ -81,9 +86,8 @@ 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(appName);
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);
@ -345,18 +349,17 @@ 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(newPassword);
s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys(newPassword);
s.Driver.FindElement(By.Id("NewPassword")).SendKeys("abc???");
s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("abc???");
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(newPassword);
s.Driver.FindElement(By.Id("Password")).SendKeys("abc???");
s.Driver.FindElement(By.Id("LoginButton")).Click();
s.GoToHome();
@ -380,14 +383,11 @@ 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();
Assert.Contains("Account successfully created.", s.FindAlertMessage().Text);
s.FindAlertMessage();
s.Driver.FindElement(By.Id("Email")).SendKeys(usr);
s.Driver.FindElement(By.Id("Password")).SendKeys("123456");
s.Driver.FindElement(By.Id("LoginButton")).Click();
@ -428,6 +428,11 @@ 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();
@ -928,9 +933,11 @@ namespace BTCPayServer.Tests
s.GoToHome();
s.Logout();
// Let's add Bob as an employee to alice's store
// Let's add Bob as a guest to alice's store
s.LogIn(alice);
s.AddUserToStore(storeId, bob, "Employee");
s.GoToUrl(storeUrl + "/users");
s.Driver.FindElement(By.Id("Email")).SendKeys(bob + Keys.Enter);
Assert.Contains("User added successfully", s.Driver.PageSource);
s.Logout();
// Bob should not have access to store, but should have access to invoice
@ -991,7 +998,7 @@ namespace BTCPayServer.Tests
var storeLink = s.Driver.FindElement(By.Id($"Store-{storeId}"));
Assert.Contains(storeName, storeLink.Text);
s.GoToStore(storeId);
storeLink.Click();
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);
}
@ -1061,8 +1068,7 @@ namespace BTCPayServer.Tests
Policies.CanViewInvoices,
Policies.CanModifyInvoices,
Policies.CanViewPaymentRequests,
Policies.CanViewPullPayments,
Policies.CanViewPayouts,
Policies.CanViewStoreSettings,
Policies.CanModifyStoreSettingsUnscoped,
Policies.CanDeleteUser
});
@ -1148,21 +1154,24 @@ namespace BTCPayServer.Tests
await s.StartAsync();
var userId = s.RegisterNewUser(true);
s.CreateNewStore();
(_, 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.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);
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.ScrollTo(By.Id("CodeTabButton"));
s.Driver.FindElement(By.Id("CodeTabButton")).Click();
s.Driver.FindElement(By.Id("ApplyItemChanges")).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;
@ -1261,8 +1270,11 @@ namespace BTCPayServer.Tests
s.CreateNewStore();
s.AddDerivationScheme();
(_, string appId) = s.CreateApp("Crowdfund");
s.Driver.FindElement(By.Id("Title")).Clear();
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);
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();
@ -1281,6 +1293,7 @@ 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();
@ -1357,16 +1370,9 @@ 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("EditorAmount")).SendKeys("20");
// 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("EditorAmount")).SendKeys("20");
s.Driver.FindElement(By.Id("ApplyItemChanges")).Click();
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
@ -2493,6 +2499,7 @@ namespace BTCPayServer.Tests
using var s = CreateSeleniumTester();
s.Server.ActivateLightning();
await s.StartAsync();
await s.Server.EnsureChannelsSetup();
s.RegisterNewUser(true);
s.CreateNewStore();
@ -2500,7 +2507,10 @@ namespace BTCPayServer.Tests
s.AddLightningNode(LightningConnectionType.CLightning, false);
s.GoToLightningSettings();
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), true);
s.CreateApp("PointOfSale");
s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click();
s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString());
s.Driver.FindElement(By.Id("Create")).Click();
TestUtils.Eventually(() => Assert.Contains("App successfully created", s.FindAlertMessage().Text));
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Print']")).Click();
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
@ -2519,10 +2529,13 @@ 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();
@ -2533,16 +2546,24 @@ namespace BTCPayServer.Tests
s.RegisterNewUser(true);
// Setup store and associate user
(_, string storeId) = s.CreateNewStore();
s.CreateNewStore();
s.GoToStore();
s.AddDerivationScheme();
s.AddUserToStore(storeId, user, "Guest");
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);
// Setup POS
s.CreateApp("PointOfSale");
var editUrl = s.Driver.Url;
s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click();
s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString());
s.Driver.FindElement(By.Id("Create")).Click();
TestUtils.Eventually(() => Assert.Contains("App successfully created", s.FindAlertMessage().Text));
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Light']")).Click();
s.Driver.FindElement(By.Id("Currency")).SendKeys("EUR");
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);
@ -2550,12 +2571,9 @@ 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);
@ -2565,7 +2583,6 @@ 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);
@ -2611,131 +2628,6 @@ 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 cartData = s.Driver.FindElement(By.CssSelector("#CartData table"));
var items = cartData.FindElements(By.CssSelector("tbody tr"));
var sums = cartData.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);
// Receipt print
s.Driver.FindElement(By.Id("ReceiptLinkPrint")).Click();
windows = s.Driver.WindowHandles;
Assert.Equal(3, windows.Count);
s.Driver.SwitchTo().Window(windows[2]);
var paymentDetails = s.Driver.WaitForElement(By.CssSelector("#PaymentDetails table"));
items = paymentDetails.FindElements(By.CssSelector("tr.cart-data"));
sums = paymentDetails.FindElements(By.CssSelector("tr.sums-data"));
Assert.Equal(2, items.Count);
Assert.Equal(4, sums.Count);
Assert.Contains("Manual entry 1", items[0].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("1 234,00 €", items[0].FindElement(By.CssSelector(".val")).Text);
Assert.Contains("Manual entry 2", items[1].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("0,56 €", items[1].FindElement(By.CssSelector(".val")).Text);
Assert.Contains("Subtotal", sums[0].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("1 234,56 €", sums[0].FindElement(By.CssSelector(".val")).Text);
Assert.Contains("Discount", sums[1].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("10% = 123,46 €", sums[1].FindElement(By.CssSelector(".val")).Text);
Assert.Contains("Tip", sums[2].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("10% = 111,11 €", sums[2].FindElement(By.CssSelector(".val")).Text);
Assert.Contains("Total", sums[3].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("1 222,21 €", sums[3].FindElement(By.CssSelector(".val")).Text);
s.Driver.Close();
s.Driver.SwitchTo().Window(windows[1]);
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
// 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();
cartData = s.Driver.FindElement(By.CssSelector("#CartData table"));
items = cartData.FindElements(By.CssSelector("tbody tr"));
sums = cartData.FindElements(By.CssSelector("tfoot tr"));
Assert.Equal(3, items.Count);
Assert.Single(sums);
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("Total", sums[0].FindElement(By.CssSelector("th")).Text);
Assert.Contains("4,23 €", sums[0].FindElement(By.CssSelector("td")).Text);
// Receipt print
s.Driver.FindElement(By.Id("ReceiptLinkPrint")).Click();
windows = s.Driver.WindowHandles;
Assert.Equal(2, windows.Count);
s.Driver.SwitchTo().Window(windows[1]);
paymentDetails = s.Driver.WaitForElement(By.CssSelector("#PaymentDetails table"));
items = paymentDetails.FindElements(By.CssSelector("tr.cart-data"));
sums = paymentDetails.FindElements(By.CssSelector("tr.sums-data"));
Assert.Equal(3, items.Count);
Assert.Single(sums);
Assert.Contains("Black Tea", items[0].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("1 x 1,00 € = 1,00 €", items[0].FindElement(By.CssSelector(".val")).Text);
Assert.Contains("Green Tea", items[1].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("2 x 1,00 € = 2,00 €", items[1].FindElement(By.CssSelector(".val")).Text);
Assert.Contains("Manual entry 1", items[2].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("1,23 €", items[2].FindElement(By.CssSelector(".val")).Text);
Assert.Contains("Total", sums[0].FindElement(By.CssSelector(".key")).Text);
Assert.Contains("4,23 €", sums[0].FindElement(By.CssSelector(".val")).Text);
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
// Guest user can access recent transactions
s.GoToHome();
@ -2753,27 +2645,22 @@ namespace BTCPayServer.Tests
[Fact]
[Trait("Selenium", "Selenium")]
[Trait("Lightning", "Lightning")]
public async Task CanUsePOSCart()
{
using var s = CreateSeleniumTester();
s.Server.ActivateLightning();
await s.StartAsync();
// Create users
var user = s.RegisterNewUser();
var userAccount = s.AsTestAccount();
s.GoToHome();
s.Logout();
s.GoToRegister();
await s.Server.EnsureChannelsSetup();
s.RegisterNewUser(true);
// Setup store and associate user
(_, string storeId) = s.CreateNewStore();
s.CreateNewStore();
s.GoToStore();
s.AddDerivationScheme();
s.AddUserToStore(storeId, user, "Guest");
// Setup POS
s.CreateApp("PointOfSale");
s.AddLightningNode(LightningConnectionType.CLightning, false);
s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click();
s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString());
s.Driver.FindElement(By.Id("Create")).Click();
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Cart']")).Click();
s.Driver.FindElement(By.Id("Currency")).SendKeys("EUR");
Assert.False(s.Driver.FindElement(By.Id("EnableTips")).Selected);
@ -2786,8 +2673,6 @@ 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);
@ -2873,59 +2758,11 @@ namespace BTCPayServer.Tests
Assert.Contains("9,90 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
// Pay
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 cartData = s.Driver.FindElement(By.CssSelector("#CartData table"));
var items = cartData.FindElements(By.CssSelector("tbody tr"));
var sums = cartData.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);
s.PayInvoice();
// 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);
// 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"));
Assert.Equal("3 left", s.Driver.FindElement(By.CssSelector(".posItem:nth-child(3) .badge.inventory")).Text);
}
[Fact]
@ -3372,7 +3209,6 @@ retry:
Assert.StartsWith(s.ServerUri.ToString(), s.Driver.Url);
});
}
[Fact]
[Trait("Selenium", "Selenium")]
public async Task CanUseRoleManager()
@ -3383,10 +3219,8 @@ retry:
s.GoToHome();
s.GoToServer(ServerNavPages.Roles);
var existingServerRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
Assert.Equal(5, existingServerRoles.Count);
Assert.Equal(3, existingServerRoles.Count);
IWebElement ownerRow = null;
IWebElement managerRow = null;
IWebElement employeeRow = null;
IWebElement guestRow = null;
foreach (var roleItem in existingServerRoles)
{
@ -3394,14 +3228,6 @@ 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;
@ -3409,21 +3235,11 @@ 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));
@ -3451,11 +3267,13 @@ 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(5, existingStoreRoles.Count);
Assert.Equal(4, existingStoreRoles.Count(element => element.Text.Contains("Server-wide", StringComparison.InvariantCultureIgnoreCase)));
Assert.Equal(3, existingStoreRoles.Count);
Assert.Equal(2, existingStoreRoles.Count(element => element.Text.Contains("Server-wide", StringComparison.InvariantCultureIgnoreCase)));
foreach (var roleItem in existingStoreRoles)
{
@ -3506,19 +3324,20 @@ 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(4, options.Count);
Assert.Equal(2, options.Count);
Assert.Contains(options, element => element.Text.Equals("store role", StringComparison.InvariantCultureIgnoreCase));
s.CreateNewStore();
s.GoToStore(StoreNavPages.Roles);
existingStoreRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
Assert.Equal(4, existingStoreRoles.Count);
Assert.Equal(3, existingStoreRoles.Count(element => element.Text.Contains("Server-wide", StringComparison.InvariantCultureIgnoreCase)));
Assert.Equal(2, existingStoreRoles.Count);
Assert.Equal(1, existingStoreRoles.Count(element => element.Text.Contains("Server-wide", StringComparison.InvariantCultureIgnoreCase)));
Assert.Equal(0, existingStoreRoles.Count(element => element.Text.Contains("store role", StringComparison.InvariantCultureIgnoreCase)));
s.GoToStore(StoreNavPages.Users);
options = s.Driver.FindElements(By.CssSelector("#Role option"));
Assert.Equal(3, options.Count);
Assert.Single(options);
Assert.DoesNotContain(options, element => element.Text.Equals("store role", StringComparison.InvariantCultureIgnoreCase));
s.GoToStore(StoreNavPages.Roles);
s.Driver.FindElement(By.Id("CreateRole")).Click();
s.Driver.FindElement(By.Id("Role")).SendKeys("Malice");
@ -3530,260 +3349,6 @@ 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)
{

View File

@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
@ -27,7 +25,6 @@ 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;
@ -35,7 +32,6 @@ using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Npgsql;
using Xunit;
using Xunit.Sdk;
@ -554,23 +550,13 @@ retry:
public async Task AddGuest(string userId)
{
var repo = parent.PayTester.GetService<StoreRepository>();
await repo.AddOrUpdateStoreUser(StoreId, userId, StoreRoleId.Guest);
var repo = this.parent.PayTester.GetService<StoreRepository>();
await repo.AddStoreUser(StoreId, userId, StoreRoleId.Guest);
}
public async Task AddOwner(string userId)
{
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);
var repo = this.parent.PayTester.GetService<StoreRepository>();
await repo.AddStoreUser(StoreId, userId, StoreRoleId.Owner);
}
public async Task<uint256> PayOnChain(string invoiceId)
@ -661,33 +647,5 @@ 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();
}
}
}
}

View File

@ -1,11 +0,0 @@
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,
1 Id Blob Created CustomerEmail ExceptionStatus ItemCode OrderId Status StoreDataId Archived Blob2
2 Q7RqoHLngK9svM4MgRyi9y \x1f8b0800000000000003c454cb76a24010dde72b7258cf180d08929d0a89899ae323ea4962163c0ae808ddd8344426c72f9bc57cd2fcc2340d31ea38b398cd2cfb5657d5add7fdf9fdc7fbd9f9b9845ce9ea5c1a6b9335e90db0dfd7936ca80cfd498ef45cfa52fc4818a1702bbec9893feb76d9ace3abd3d6e06e456dd76b2fece46eb8eee4d7032f9bae5f6fd44dbfb330ddd2995017a870c6691896f16200774442e4e41cae0b8c5a0cf8436dea6a4d56344d5135596e2ac286704690030f282abe349a724bd6e5a67c298cb089117746041fd815a5b2bb109304b1b6eb524812514347ef1bb2a6cd0686663e7a8b4779dcf3bd45eb5ae9bcb181e10f50c93ca6c44d1d768b3d422391817b172d2b2831880c489c229e0d4085478577890b7be51691823c418e1572d4b3c2043e60caabe29856ab578893520a58b4459a4d0d89a35bc1c54e73dec5534c84e5de8a8e520ad88c2c149ec0bb24c58ce6272c4f283e814e59399ddfe220762a48d5ebcb3f9b1a274ca380e08f24bbbaf9ec0c8b59fbdbc3d70965a20953566c8d2fbab589535b35dab61f78e9a4bb50c76b2af7bda9f18a56caca9ca1acbdf202e7a6375ccd4d50eca775539e8fbf69fa4a514c326e0e8d7870d7efb747fd66369faba38d366b4c641dc6c8c67660240b3d35c78dbe1be85dc38efcc9d7e7f832095ea4d38c1088457b5f4a9d87ee52ba5afe277a4b69fb71c1164b05270c6f5275370ec425e7cab6eb706ce511605660cf2fe575829762d7b243d85fe10a1e1e2e19475d44c161b3c9a0c818301627571717919530a0981f0767b342d8af39242ab9b0cd3588d3ad9762e0f150f784218f1f4d41b160c2685a26c57b8632c5a7b000cd80ce68789817610cace642446a36737875e5bf1aa17e99dfa179cc48b568d55df1c9ed1e7fd7a7f296cb9e0d8105a410bbf7edcee4014c4aefc60e3baa5860ffa454c279bbbb978860c4d59a77d7dce9e24e135bf54a130354487aa14855323898bf95f18916c3aeac3d2b090e7fc0860176c13d1ed2e76a40566dd0f1563d9010a88585f0356af5b3ed2f000000ffff030035140a5d88060000 2018-10-01 11:32:12+00 expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd f
3 Ka6GHBrFJPRwFRga1RD6Yz \x1f8b0800000000000003c454cd769a4014dee7297258b74682a066a7427e24e6284a729a9fc5001798083364188826c727eba28fd457e830101353db45375dcef7cdbdf7bbbf3fbfff783d383c5470a09c1c2a3632cece87ec743c759e4f9d08a98e697c7b51be543f724e195cc86f5a1eb9a31177879131ef5d8e97cc0bc2c18d978f274fc3f5e96558ce9f1ecf8c953dbcb182da98b2009834264592d4fe3280604a13ecaf05dc9618431cc44337ba46abad6aed76db50b5635d72989414fbb0c069f545d5b59ed6eff4f4da10561916c698921d5eef367c0019cd311f0401833c973998b7a5e742ef7110a7fd65e9f62889c7b6f5726b777d3fc25c9fd5ca334683c2e71724a42c9511847555b24a1287d484dcaffc9d310072b80024cd1a724403f89073e52e5ee7d84789404394e4f00633915a558656fbb881fc823120b2388ae53a8a4037529157ac452df7e991cc154a3fc594b095229cecc147b4209cadf730b738db83ce79dda3dffc60becf4953f1e33f53ea1e6a1a53f216649bb7e8a08938fa384362a870298b30e7d5ec44b25aabacf00c73e045715838a31b63f6c4343b9c9b8f78d9595a2e2e07cb30f6cfce27cb6b0b3adeed93ae5dcf5ebafd65a763d1993e31b3cbb16d0fa6b65e5e5f1bd355d7551dad0f33ec112f36f39b7e61cd543b88fb23d34b23e7eb5d769cc70fca7e4518e4b8bdde2bc3c5e85e39b9ff4ff2ee95cddb1e235e484d049e95667b7cc86acd0db7ad7086d629105e61770ff58e4258900079097c9ce1069eec0e994003ccc0e7ae7359458c39cff293a3a314e51c1811db21d42c31895a3e4d6b2d7c750a7281dbf5e686c2d515e538145b5349ac947056d441c907a20ef17e5e8095c05c96ecc6c584006f0590d296c77d915dfdaf455954c7f7d93ae3b419b466af44e7b68fbf5fa97a99eb9a4d80c7b43a79af9b2d150238b5b5bac53e652cb17fba57d278b3dd9794122c6eb6a8aeb5bd8edbcbd8d79acb18e3eab05727a909063bfd47a5e868d5ec863d4779bcfb03561c4800c1e726bd8f0694cd047d9eaa054d8021222f9fda6a1f6c7e010000ffff0300ddecc9e08e060000 2018-10-01 11:54:10+00 expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd f
4 Q3kZ3F8cUD57WUqcc8QLs2 \x1f8b0800000000000003c4544972da4014ddfb1494d6099110a377806463a662b0c085f142c3176a23758b564b86b83859163952ae90564b96812259649365bff77ffff9fdfaf1f3fda654929023dd96a4a9ba5da9774ddbd06a8da5b1b3ede6741855a42fa945c408850761a6461ba3db654667539f3787fd2db51cb7bdb4a2fe68d739dc0ddd64be7bbdafef079da5ee64ce843a4085338e7d3ffb2f047026c447f681c3b2c0a8c9803f6af546bd2c2baa2ccb7545add404877042900d8f28484d949ada545bd566a32248d887883b2382cff85a23e71d08498458db71284491a861921876af3f1ec361bc5bb4d427dadb57bcfba145f4fd66fe36795a6599879438b1cd1eb04b68202270efb465694a0c020d223bfd6f64460c28260e94e6ccdc22bc11feb95597e327c5a7ff7a8708d9a6cf51d7f423f88029af916395b29c23764c2960d12449376612478f22332b3ef09e5ecb4b306333b80829603d30917f05ef9218337ab8c2ac507805e545b26bff7711bbf649def9ca9f29e50a35f108fe0852d4cd27a999cc3cdd25be5c28114d98b3748736a25bfb30b6ea5adbda786e3ceb2eebd31d5507ee5c7b45dbea563750d2deba9e7ddf1b6d173a54add5aea62ea6df1bad6db5aa93696da485c3fe60d09e0c6ac962519fec1b8632535b304516b63c2d5ab6627daa0c1cafd5d5ac6033fbfa1c5622ef45ba9e1102b176ef6ba9f3d85d4bb7ebff94de5a3a7edcb3c9629113863729bf221bc22ce79c2b3a1c9a8700304bb1e797ec56c18db1635a3e9cae700e8fce978ca30ea2603363364c237a8c85d1edb76f417134517633659b04592e6c7f07e290e54c1a5cfed59830e4f2a349534c336134ce82e213220bf129334013a006f5cfe3228c81951d0848d96236af2eb32b139addad64d343c848be68f95df1c9158fbfab5576cb59cf46c03c924adffbb1a05c8059e6ad14d845c502fb27dd12cec7e25e028211d76ede5dbd50c942215b6aae901e4a053e55a43c189ccddf4cf844d361e76ccf8cbc730bd833c00e389743fa5c0d48f20dbadcaa47e20335b1103ea52cdf1c7f030000ffff03003e6b8efb96060000 2018-10-01 11:54:32+00 expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd f
5 FSktP1Nrxu7arh7TUFAgTZ \x1f8b0800000000000003c4554b72e33610ddcf295c5c27b23e16297a1599923c63591efd55f1781620d1143124011a0035545c3a59163952ae10008415d9a54caab2c992af5f773ff48f7ffefec7cb878b0b8760e7fac2192d52396d3df0aaf4104fbce56ad4df2e1f9d9f344348c6e193a175c47615047275b37517bdfbbb948738ee6f42713779bed98feee3dde2f9dbad5b8d6f36435c3b338e81d7ce41a922e59f2d50872e00f0946524da2b46d3601c49501f5dd76b377a6dbfedf7bc96e7f9c646e88e91089624d79456b7d3ebf8dd66a76b8c501544391346dfda7d6bc7503041641f630e4298e70c969bd093de7886d63de1a2ea7b67ddc28fb76595dcfdba1db9eeb296597086cb487ea231e3b9c9a0bc75f5b42409f90044a4e34d9090c029c370b1902825746bfc2d2b50b862d132cb2c5a247b41229429344699805798ab376afdcd66a369b1a8e41ca82993335ccd1d851e8cb6b0dcab7a9e53662c0f287f97d4c0c31c119d56c5d54d01fe0b54282f3268442c774e99012ba9e4fb33311e497106550f97e73206449e0b62bbd1fe6753eb8c699a30fa9ae45809d5dd0192e884ae5acec9ce946521f55c6d4dfdaaa20cdd413fdc2671390f36eeec9977c6f162f08da457e9704576fd344ea2db8f93743d84abf0f1b9db59cf7ef3fcf4ea6ac866ddc9a0b8bf1b8ffbd37177b75ebbd3ca5bb5e61d1f6624a46132101bbf1cce5a639cf8c120ccb7f39fbf146d917c75ce2b226046f1e5c9b959064fcef5d3ff24efc939bcae3b92a5d144e1bb63372b82a2d66c6dc70a17689f03951afbf2b5de5f884b8a5198c1e9585b78f26f63a778987088e46a7eaf89899485b8bebc3ce15dca04d154ec59597bc86a04765dcc77acb43d304962b55a5ab4d6267959cba027861fa4504b9985284a85ad09f01df015cf4ef96a852805d9c090b3462823558a9ad760bc5e7c27e2fb42323b95762d559b8f1f3f3e77f531a80b3c0199307d465f0e47530c30afbd5b47ec5d310cf69f0e9f713e1c972b6794a8ff803a69c3e3993d9e58bf6b4f6c42f4cf429f349b0cde0c0bdaa9f6ebc9b0d68f48246f195049a018f0fbfefd3d47b0b3e3f67e04972c038e687d391bcd0f87bf000000ffff030075db901fe2060000 2018-10-01 11:57:15+00 customer@example.com CustomOrderId expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd f
6 HuzCsv9hghew2FD6zVqUyY \x1f8b0800000000000003c455cb76d33010ddf3153d5e439aa75377456aa794268126a913286521dbe358b52d3992ecc6edc997b1e093f80564590d694f8073d8b0f49d3b3357f3f28f6fdf1f5f1d1d1938304e8f8c8bfcc1e68515ad22b86f9f3be6c362ed969f8dd715830bcae0bda275f8cab56de19eadccf9c9f832665e100e961ebf9caccfcaf37158ccd777efcccde86c390c6a67ca0260b5b39dcb48e9470dd4a13380e08a26d82f25a3a9308604c88f9ed96f374eda56db3ae9b7fa7d4bd9302928f6e11aa715a5d5eb9c74ac5edfea28236c322c9d3125cfeca6a9ed01649463310802069cabe758e9f2be701d7ad3b3379f9069a72be406a865ad3fdf59e36ceab46a9919a341ee8bf724a42c5519a47755bd4a9280d401ee57f126880b60840670341728c664a5fc35cb96b864913c49349a4525c73e4a241aa284c313cce41b2bfdcd66a3a9313f670c882a933174678644b74a9b9797b29e879429cb0794be48aae0618a709556c6ad9a02ec2d6c509a25d0f0696aec336d9a13c1ca03316e707600950f178732da581c0aa2bbd1febda975c0741551f294645709d95d0709b447972d67b85065998b6aae56aa7e9b2cf74c67e0ada2309fd94b73ba669d513877ee70dc8d872e2e067118f9ef2e26f162085def66ddeb2ca60f7d2bee768774da9b38d9f872341a5c8d7ac562615e6dfa6e6bd6b1608a3de2450e5f5af970da1a0591653b5eba9abdf992b579f4d538ac08831ac5c75be3ecdabe354e6fff93bc5b63fbb4ee48e44a13817b436f960f59ad59db7615ce5099021115f6e56bbdbf10e624405e02fb63ade1c9dfc64ef202ccc017ee6c5c112321327e7a7cbcc73b161122312f695e7b88cd39e87551dfa1d4f6810a1ccad5aa4457da04cb6b1964cff087147229130ff931d7350156007359b2cf972b4408884600296d78c297a5a8790dcaeac5377c566682eaa9d46b29dbbcfbf8f3b9ab8f415de00988885667f471bb338500b3dabbb5c35e144361ff74f894f376b75c292558fe07e4491beeceac3eb1dd66b3ad4f6c84ab9f4575d2743278362ca890edaf26435b2f108f9e336023800410bcecdfaf3982428fdbcb11bca6093044eacbd968bedafe040000ffff030069b828dae2060000 2018-10-01 12:09:53+00 customer@example.com CustomOrderId expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd f
7 YZV1PQasUKEXdSkSV69XDL \x1f8b0800000000000003c455cb76db3610dde72b7cb86e65911429cbabca1463c792523d2c39759c05480c4584244003202bd5475fd6453e29bf50108455d9474dcfe9a64bdeb93373312f7efff3dbf3bbb3338b60ebf2ccfaed616dcfe648acc6e127bccc966b7ff06934b17e6a1842320e1f34cd159b5510c8d5d5c65f5e4c6e331ee164781f89dbe9d3d5eefd24a9974f5faffdedf8ea3ec4ad33e31878eb1c542a52f1ab01dad025009eb19cc43bc5e86a8c2309eac3f37da7e3f56cd7f55dcf751c6d23b46624863b523414db732fdc81ef38b636c2b624ca9930facadeb78d1d43c90491438c3908a19f33b85927f2b62f2807a7866c7ce1f5c49dbc76275e9a08e7a2ba6e65969ce12a961f68c278a13328efa67a8d2409c50844dcc49b2221815386e16c295146e846fb1b56a070c5a2559e1bb44c7782c42857688272012f30576f6cf477bb9daec1e28a73a0ba4c56b85a580add6b6d51b553f53ca54c5b3ea2e24d520d8705224d5a15b7690af05f608b8a32874ecc0aeb9819b08a4abe3b11e381942750f570792a6340e4a920a61bce3f9bec13a659cae84b92432554774748a223ba6a3927b52ecb523673b5d1f5db9655e48f86d1264daa4570efcf9fb83b4e96a3af24eb65e18ad4c32c49e3eb9b69b60ea1173d3c79ee7afe477f90f57a219b7bd35139b91d8f87b3b157afd7fe6cdb5fd90b77007312d1281d89fb4115ceed314e07c1282a368b9f3f978e48bf58a71511d0a3f8fc685ddd058fd6e5e3ff24efd1dabfac3b9295d644e177cb6c560c65abd9d80e152ed1ae002a1becf397767f21a92846510ec7636de0e9bf8d9de261c22196abc5a421a65296e2f2fcfc88772e534433b16355eb21b7efc1ac8bfe4e94b68f4c9244ad5623bad12679d5caa047861fa4504b994728ce84a909f01af88ae7c77cb5429482ec6028582792b12a45cbeb30de2ebe15f35d2999994ab396aacd878f1f9fbbf618b4059e824c5973469ff7075302b068bded03f6a6181afb4f874f3bef0fcb55304ad47f409db4f07066cd89ed75bb7d736253d2fc2c9a936692c1ab6141b56a7f3319c67a8344fa9a015b0914037edbbfbfe7086a336e6f47f08ee5c0116d2f67a7fb6eff17000000ffff03003a3b8e5ae2060000 2018-10-01 12:17:01+00 customer@example.com CustomOrderId expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd f
8 3qtdXmycQwhWEPs91MExB2 \x1f8b0800000000000003c455cb769b4810dde72b7c5827b21002455e4502f2b0a48c1e96e538cea2a10bd106ba5177238bf8e8cb66319f34bf90a6c18aeca324e7cc6696dcba5575bb5efcfbf73f8fafcece0c828d8b33c3da487c9395e1ec215ef953d13727fe6ed8315e570c2119874f354dac97ae2b97c3b5b3783bbe4c7880a3c12a109793cdb07c3f8eb68bcdfd0767371aae7c5c3b338e81d7ce6ea122657f35401d3a07c0539692b0548cb6c63892a03e6cc7e9b4ecae69598e655b9d8eb611ba6524842b925514d3b6de5a7dc7b11d6d845d4e943361f499bd6737760c3913440e30e620847e8ef9b0bcf70a6bf6ddc3e3e48bdfe38e7f39ef4d2837931b424641af969973868b507ea211e399cea0bcabea559224641e88b08a37414202a70cc3d942a284d0b5f66f58aec2158b1669daa0795c0a12a254a1114a053cc15cbd51eb373bad76038605e740759d0c7f393714bad7e282a254053d254d5b3ea3ec45560dfb1922555e15b7ea0af077b043599e422b649971cc745941252f4fc4b825f90954bd5c9ecae812792a48d38eceaf4de609d33466f429c9a112aabd1e92e888ae7acec956976521abc15aebfaedf22270bc41b08ea362eeae9cd9865ba368e1dd93a49bf84bb21d24511c7ef83849ae7de806b71bdbba9e7deff5936ed767337be2e5e3cbd168301dd9dbeb6b67baeb2dcdb9d587190968107b62d52ffc9939c271dff5826c3d7ff335ef88f89b715a11013d8b8f77c6f0cabd332eeefe277977c6fe69df912cb4260a0f46b35a21e4b5e6c676a8708eca0ca8acb0afdfea0586a8a01805291ccf75034ffe34768a870987502ee7e38a184b998b8bf3f323deb98c114d44c98ada43eede83de97767d4122a5ed33932452bb5589aeb4495ed432e891e13729d456a6010a13d1d404f816f892a7c77cb54294826c61c8582b90a12a45cd6b315e6fbe11f23297ac99ca662d559b0f1fbfbf77f535a80b3c0119b3ea8e3eee0fa608605e7b9b07ec453134f69f2e9f76de1f962b6394a81f81ba69fee1ce3637b6db36cde6c6c6a4fa5b5437ad4906cf86056d55fbabc968ac1f91889f3360278162c02ffbf7738e60db8cdbcb11bc62297044ebd3d96abfdaff000000ffff0300fd61e9cae3060000 2018-10-01 12:24:16+00 customer@example.com CustomOrderId expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd f
9 BtJGr7kt52QkZe8RdhKz42 \x1f8b0800000000000003c455c172da4810bde72b5c3a6f301208249f168462078cd780c115e21c469a161a4b9a9167465a888b2fcb219f945fc86824b3d8c566abf692a3de7b3dfda6a7bbf5e3dbf7e777676706c1c6c5993194e34bde4fa46dcd923538731c4fbe762de38f4a2124e3f051cb3a62b3f43cb91c6e7a0be77a9cf0004783fb408ca74fc3dd87eba85c3c3d5ef6b693e1bd8feb60c631f03ad82bd449d95f0d501f9d03e05b969270a7146d8d7124417dd83dc76a5996d9711dc771db3dcd115a3212c21dc92a8969779c8edb6ff72d4dc236272a9830fa9a771b1e43ce0491038c3908a1af73152d3ee55bfea9fb784978de21ebf286925537598ef12258737f52dbcc39c345283fd288f14c6750d155f52a4b12b21188b03a6f8a84044e1986b3854409a11b1ddfa83c852b152dd2b441f3782748885285462815f002737547eddf6cb79c7ebf6d9b96dd7061c139505d2ec35fce0d85eeb5c7a0d8a9ba9e72a8991b94bd49ae613f43a44a6fa4880b4168028efbe7a6025b21cb8c63a9c70a2af9eec4216b929f405505e4a9941e91a70e699ec5fa77ca3c41ddc68cbe243994423df308497424576fcf49a9ebb29055836d7401b77911f44683601347c5dcbbefcd9e7867122d468f24e926fe929483248ac3cbab69b2f2a11bac9fecce6af6b5ef26ddaecf66f674945f8f2793c1edc42e57abdeedb6bf34e71d176624a0413c12f76ee1cfcc098e5d6f14649bf9fbcfb925e22fc6694704744f3e3f18c33befc1b878f84df61e8cfdcbdc2359684f14fe369a110b21af3d37dca1c239da654065857dfe520f324405c52848e1b8bf1b78fa9f7da784987008e5727e5d2963297371717e0e5b94e52954ba7319239a881d2bea08b9fd007a70daf52a8994b91b2649a486ac725d9993bca87dd023e21729d478a6010a13d1140578097cc9d363bdf24d29c816868cb50219aa5ad4ba16e3f50a3042becb256bdab2194cf5ce878f5f2fbe7a2dd4159e828c59b5509ff7072a0298d7d1e6017b530c8dfdaf15a883f787e9ca1825ea8fa0969b7f58b8cdb2edb64dbb59b631a97e1bd5726b92c1ab6e41a57aeaaa351af60a89f8b502b6122806fcf6fdfe6924289b7e7bdb83772c058e68bd435bed77fb9f000000ffff030034f07389ec060000 2018-10-01 12:31:12+00 customer@gmail.com CustomOrderId expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd f
10 VWB3riH9WuCv9NtSedaGE6 \x1f8b0800000000000003c455c1729b4810bde72b5c9cb3b21012029f222162c792bc9664a494631f0668c404984133032bc5a52fcb219f945fc83060457629deaabdec91f75e4fbfe9e96e7e7efff1f4eeec4cc3a17671a62d574383e12b7b5538a57d231610a24bd7d4de570a2e28834f4a66f0b5e738c21baecd8535b94e981f468395cfafa79be1eee3242a179baf97e6763c5cb9611d4c5908ac0e760a7952f67703d447e700e12d4d71b0938ab6c21812203f7aa6d569753aba615b9665b74dc56152521cc01dce2a89de332cc3eeebb6ae48d8e65806634a5ef056bbe143c829c76210860c3857d7e96fd6b6037d4f1457c10adb9fbd9bfb8531b059d17326cbcbd5106a9b39a36111884f24a22c5319647455bdca92806c043ca8ce9b222e80111ac2d942a00493b58a6f548ec4a58a1469daa079bce33840a944239472788699bca3f2afb75b56bfdfeee99d5ec30505634054b934d79b6b12dd2b8f7eb193753de5503137287b955cc16e8670955e4b11e31c93042cfbc3ba025b01cdb463a9430b22d8eec421f7383f81ca0a8853291d2c4e1dd23c4be7cf947e82ba8d29794e7228857ce61112e8482edf9ee152d56521aa065bab026ef3c23747037f1d47c5dc5999b30d33c6d162f41527ddc4f5703948a238b8bc9a264b17bafefda6672c67dffa76d2edba74d69b8ef2c9f5783cb81df7cae5d2bcddf63d7d6ed830c33ef1e3115fd9853bd3c7616c3b233f5bcffffa927778fca89d768441f5e4d38336bc731eb48b87ffc9de83b67f9e7b240ae589c03f5a336201e4b5e7863b543847bb0c88a8b02f8ff52043549010f9291cf777034fffb5efa430c40c02e1cd2795321622e717e7e7b045599e42a53b17312209dfd1a28e10db8fa006a75daf92489abba1024772c82ad79539c18ada073922de4821c733f55190f0a628c04a601e4b8ff5d23721205a2164b4e58b40d6a2d6b528ab578016b05d2e68d396cd60ca773e7cbcbdf8eab55057780a22a6d5427dda1fa808605e47eb07ec553114f69f56a00ade1fa62ba304cb3f825c6eee61e136cbb6dbd6cd66d9c6b8fa6d54cbad49062fba0595f2a9abd668d82bc4e3970ad80a202184afdfef772341d9f4dbeb1ebca3293044ea1dda6abfdbff020000ffff0300e1a26ae9ec060000 2018-10-01 12:33:11+00 customer@gmail.com CustomOrderId expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd f
11 At4CDM5vfewV2WDHEPKRbt \x1f8b0800000000000003c4554d73a33810bdcfaf4871de758c0db6c9696d20c9c471c61fb15d35933908688c06908824183329ffb23dec4fdabfb092205e27e59daddacb1e79efb5faa9d5ddfcf9fb1f2f1f2e2e0c1c195717c65858ae37b3ab18be6f7a5befd69f4f9781307e510a2e28838f5ad6e7bbb5eb8af56437588deeef521644f1781bf0bbd9f3a4bebe8fabd5f3b79bc17e3ad9fa51134c5904ac09764b7952fea9059aa30b80684e331cd652d1d5184302e4873d18f53b3db33f1c398edd1d3a9ac3a4a23884479c2b8969f74756d71c0d4c4dc2bec0321853f286ef0d5b3e8282722cc651c480737d9db1fde361b901320bf8c4ab83bb38c2dc22d73dd7de06f5eac6228dcd82d1a80cc547125396eb0c325a554f5912907bc04375de0c71018cd0082e5602a598ec747cab72252e55a4ccb2162d929ae31065128d51c6e11566f28ecabf351a76468ee90c7bc3960a4bc680e86a19fe7a6948f4a02d06652dcb7acea0661e50fe2eb786fd1c6195ddc810e31c931446ce6f3b0576429a1ba752979644b0facc219f717106950510e752ba589c3ba47d95de3f53e6196a9e50f29ae4580af9ca1e12e8442e9f9ee14ad76525547fed7401f745190cbc71b04be272e96e078b67d69fc62bef1b4eadd45fe36a9cc64978733b4b373e58c1e767bbbf59fc183aa965f97461cfbce2fe6e3a1dcfa776b5d90ce6fbe1da5cf61d58e0800489c7b74ee92fcc699438ae17e4bbe5af5f8a1e4fbe1ae71d61d02df9f2644c1edd27e3eae97fb2f7641c5ec71e89527b22f0dd68272c84a2f1dc72c70a17a8ce8108857df9dacc31c425895090c1697bb7f0ec5ffb4e0a23cc2014ebe5bd52264214fceaf212f6282f3250ba4b912092f29a964d84d85f839e9b6eb3496269ee810a1ccb1953ae9539c1cac60739217e92424e6716a030e56d518055c0d62c3bd54bdf8480e84490d34e2042598b46d7a1acd90046c8ea42d0b62ddbc194ef7cfcf8f9de6bb64253e1198884aa7dfa72385231c0b289368fd8bb6268ec3f6d401d7c384e574e09963f04b9dbfce3be3deeda41bfddb509567f0db5dbda64f0a65b50259f5ab546cbde229ebc55c05e0089207aff7e7f3712546dbfbdefc1479a0143a459a19dee87c35f000000ffff0300011a2ad8eb060000 2018-10-01 13:51:01+00 customer@gmail.com CustomOrderId expired 3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd f

View File

@ -1,11 +0,0 @@
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,,
1 Id Blob InvoiceDataId Accounted Blob2 Type
2 a8be47ca93a1583f60b25fb1279b6c162af731024d169f8d1cd0d29d1d391132-0 \x1f8b0800000000000003748f396ec33010457b9f4260ed82c39d2ead20958b2050a966c425201289824205100c9f2c458e942b447216b8c9af66fe03de603edf3fcebbaa22537021bd05dfa43e904305921b4139a36cbfd13c9731a7a1ac847006c02d78eb3df3149cf13682129e320e513304a59ced98ee40c68e29caa33408963b14ba0b68e84fc89f79be7a9d1692de04345a10d26b0116c0482339a008144da7509a2e4aca95d7511923b534fadb87cee57928c1afca32cde15aba69194baeb3df5e23c7a62637f5032e7d18ca1d16dce8b925751e629a7a2c290ff5666bc901ecbe258fc7fb75bc6a5b720a4fe896758ff8f21a2eff289b65fc3d7a4acf81ec2e5f000000ffff03002eb110c572010000 Q7RqoHLngK9svM4MgRyi9y t
3 c703dd8ae34622dfb96d677260631c39af483918d9e63676839be0db1b8a9268-1 \x1f8b0800000000000003748f3d6ec3300c85f79cc2d09c413fb644658c8b4e198ac2a3175aa20ba1b565b8720123c8c93af448bd42e5040dbaf44de47bc0f7c8efcfaff3ae28d84c8ec207f9260cc40e85a84a59951c14df6f695cd214c39872c2345809283adf11b71d28a39526edad00654bc05e59e1b2c58dd446fbcefa5eca529302f49e2b67b8e057b13b79b971bdecace37709835694d0a1e08012786572a3b2d944aba4ef2de81e205f50213907e6c643e7e23226f21999e685aea69bd729c53afaed35766c6af6c77ec275a0313d60c22d3db7ac8e631fe601538863bdd15a76a8a4d8b7ecf9f898e72bb765277a41b7e6bdc7b777bafcc36cd6e9b7f5145e89ed2e3f000000ffff03000f0bf2f373010000 Q7RqoHLngK9svM4MgRyi9y t
4 f237a3d75018f4435927f5b1be1a8902045d88ddda39c25254214c3e484bd383-1 \x1f8b080000000000000374904d6ac3301046f73945d03a14fdc6a32ce3925528a564e9cd581a1593da0a8e9c62424ed6458fd42b547669e8a6b39bf7c193e6fbfaf8bc2e964b76a1fedcc48e6d967c35ed3d396a2ee40f4d4b190aa3a5e5609599d38ed27bec8f3b9a32fec0a791731287748a4d973267a0bcd2b50645dac96c30d249653d7a0f603cd75c821558532d829185b15a052dc0705fa05245905ccc62ceeee661f66ac36bedf97d44815668a851704009dc14be26653344aba40f16d601c0d660909c83e2c787cec5a14be4b332f503cdd0f5e329c532fae930b63d94ec0f7ec6b1a52e3d62c229bd56ac8c5d68fa1653aeae9c6c15db88756e6a55b197ed2e6fb3b9624ff7c232cb8565b4a75774635e03be9de9f6cf4387f1f4fb957d7324b6b87d030000ffff0300eef4b353b2010000 Q7RqoHLngK9svM4MgRyi9y t
5 aedd36b5c976e56543cd7a5e207ea13dc801764cb3682474ab75a54399f20c27-9 \x1f8b08000000000000037490bf6ec2301087779e02794695ff3b66241513aaaa8a91e5629fab8812a3e050458827ebd047ea2bd44e0bead29b7cdfcffacebeaf8fcfcb6c3e2767ec4f6decc872ce16a5efd1617b46bf6d0f58a052dc68658d9cd20ed37becf76b2c197da0a5f894c4211d63dba5cc0937d4056ead14a08c82c6482e75d508e98ca6cc55c20333c8a94203ce4ba1152aa39d6d94d0de03523b8929b99b87c99b6779c128a3bfc50c582621d0c02b69a571ba623a7068b4e516adb0547b45196f022aea2bf3e303e7e2d025f45999fa0127e8faf198621d7df918596d6bf2073fc378c02e3d4282925e76a48e5d68fb03a4bcbabad87664c9163bf2b25ae75380b713e6eee9bead0cf3b632dae02bb8f176e7facf94ed78bcbd63d3ee91ccaedf000000ffff030012c428b2af010000 Q7RqoHLngK9svM4MgRyi9y t
6 3358197dcb30420d3487516e8ec9dcc0f6d03f16b4f41b77cd3ad429b1e529ee-0 \x1f8b0800000000000003748fb14ec3301086f73e45e4b9839db3e3b86383983a209431cbe57c4116248e828314557d32061e8957202914b170d3ddff49dfe9ff7cff38efb24c4c4c1cded8d7a16771c894d18536e024ec371ae734c630a49508e6dcb16955eebc06246fad6a3bdd6a5540e7655790f444aee4828d2a2d68e9750e925aeb95332580fc19f16b9eaf5e50680dc8fcc69545a774993b7285c1429adc90266d491a8980502290b2d2b80ebd85d27efb9028ce4362bf2ad334f335a4691953aca2dfaa89635d893ff1032e3d0fe90e136ef4dc882a0e5d987a4c210ed5666bc441ed1bf178bc5fb70e5f5e79bd4efc84b4dc82cb3fca7a196f4f4fe199c5eef2050000ffff0300b173d7d272010000 Q7RqoHLngK9svM4MgRyi9y t
7 03d66d2d633dac96cc45546a0555cb917f5d2f0d37dddfdf67e48f88ae728aa1-5 \x1f8b08000000000000037490bd6ec2301446779e027946951dff33928a095555c598e5625fa38812a3e050458827ebd047ea2bd40e2aead2ebe99e4f3ab6bfefcfafeb6c3e2717eccf6decc872ce1665efd1617b41bf6d8f58a0144652a3f93ded307dc4feb0c692d1275aa69a9238a4536cbb94390166405780c6988042691ff2f15c535f05e975b0cceda4a4528114423a671538ee15afbcf25e514ee524a6e4611e26af50a085a68f611a2c1312ad1396a3715c280f2cd8c0b50dc2535571b6432a0ce74aed8cbefbc0b93874097d56a67ec009ba7e3ca558475f3e4656db9afcc1af301eb14bcf90a0a4d786d4b10b6d7f8494abab8bad214bc9b45a34e46db5cecb246ec8cba3afcc725f196d700f6ecc6b80f733defeb9673b9e7e5fb2690f4866b71f000000ffff03007bb33a2fb1010000 Q7RqoHLngK9svM4MgRyi9y t
8 dc130d025a4bd2e7ee83b7707d850e7c1a52872c222a5bc2e05d3357e79aa762-1 \x1f8b08000000000000037490bf6ec2301087779e027946e8fc2f761849c584aaaa6264b9d817145162141caa08f1641dfa487d85daa98abaf426dfefb3beb3efebe3f3369bcfd995fa4b1b3ab69af345ee7b72d45ec9efda13e5506bae1557d64eb4a3f81efae386328325e4121309433c87b68b296785405322196da4d49ec0095d0b1442386bb4e0681c90d5c61ba88d954464bc50b54610e0b9f40ef82406f6300f9317ea74c5c1a3b8c1922b4da553a524eba42a3cf2a66ca4291be5a11092d704ca4a5914b5353f3e742e0c5d249f940dbe5d684a5d3f9e63a882cf3f63eb5dc5fec42f389ea88b4f1831d3db9e55a16bdafe8431edaecaba3d5bc162cf5ed79b748afd40a9797e6c2bd365e65b3aa01b533b4dbeff3364379e7f9fb16d8fc466f76f000000ffff0300f93b039faf010000 Q7RqoHLngK9svM4MgRyi9y f
9 afc39884e024cbb3a48ca997b7e878b199d85fe97f90f94471364cc866ba83ad-1 \x1f8b080000000000000374904d6ac3301085f73945d03a14c992f593655cb20aa5942cbd1949a362d258c191534cc8c9bae8917a854a2e0ddd7476ef3de6d3e87d7d7c5e17cb25b9e070ee624fd64bb62a7a4087dd05fdbe3b6231eb9ad5a21246ce698fe93d0e872d968c3ed032d59cc4319d62d7a7ec13f09a5b90d269e1b8544c88600c55014d1dbc36c632a5515b651418ed4058ee6c25906a91258740d90ca6e44e1e672eb5402b47efc3f23e13351a270cc7bc2aa407164ce0ca04e1a9ac38b34885e65c4aabd50f0f9c8b639fd067641a469c4d374ca7149be8cbc7c866df903ff6334c47ecd3232428e9b5254dec43371c21e5ea9a426bc95ae59e562d79d96cb398c12d79baf795bddc57b676f80a6eca32c0db196fffbcb39f4ebf97ecba0392c5ed1b0000ffff030018cc54a1b1010000 Q7RqoHLngK9svM4MgRyi9y t
10 6f2b8513ebfdb14b41d4de235f050cd028400cb01fde700d72c6bedc2ad8af1e-5 \x1f8b08000000000000037490bf6ac3301087f73c45d01cc249966425635c3285528a472ffa732a26b5151c39c5843c59873e525fa192434397de22ddef83efa4fbfefcba2e964b72c1e1dc869e6c977495fb012db6177475db610e85908a6d24c04c7b8c1f6138ee313358432e369330c65368fb987242517ba79876d6a0952503578243ea0d80e5c0944b2708e159e1d0714eb931d43b34b4508219e941cc62200ff378f752c719c2a368a937942b445158855e6967b8b05c0a2e9934c0b59585960c0b55d272a3cabb4f5b1bc63ea24b4aafdfcf38a776984e3154c1e59f915d5d913ff18b9e3aece3938e3ad36b43aad0fb76e8744cbbabb2ae215b5835e475b74fb7388c989ae7c7b6325d677ec0376da7d4ce936fff0ca9a7d3ef330eed11c9e2f6030000ffff0300b09cf40daf010000 Q7RqoHLngK9svM4MgRyi9y f
11 3a6659e189f83f649c0010305def1b8fb78efdbe8c0ce44d56c8c22fff742b55-61 \x1f8b08000000000000037490bd6ec3201485f73c45c41c5580f973c6b8ca14555595d10be65e222b8d891c9cca8af2641dfa487d8582ab585d7a27ce39f001e7fbf3ebb6582ec915fb4b1b3ab25eb255d63d3a6caf08fbf684d994525121a85653da61fc08fd718b39a34f340f9f9230c47368bb987c22256fb4f09e7bc79d914a000aea8c6bd083c1461bcfd2524241593a5f3a250aef8d2991c952a9c21630812999c9c3c4d55615cad37998b625131eb8e5c080496c941525958c97b2708c838286022ac5b56060f42fcf3a17862e222464ec079c4cd78fe718aa00f96364b3afc81ffbd58e27ece2b38d36a7b79a54a1f36d7fb2315557655a4dd64c30b1aac9db669b84b7ef174cea652e2c99a9b064edf060ddf8d873ffe7a2fd787e3c65d71e912cee3f000000ffff0300a991d8c4b2010000 Q7RqoHLngK9svM4MgRyi9y t

View File

@ -272,8 +272,7 @@ namespace BTCPayServer.Tests
"https://www.btse.com", // not allowing to be hit from circleci
"https://www.bitpay.com", // not allowing to be hit from circleci
"https://support.bitpay.com",
"https://www.coingecko.com", // unhappy service
"https://www.wasabiwallet.io/" // returning Forbidden
"https://www.coingecko.com" // unhappy service
};
foreach (var match in regex.Matches(text).OfType<Match>())
@ -465,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}")).Content.ReadAsStringAsync()).Trim();
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}/lib/main.min.js")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-sortable", "sortable.min.js").Trim();
@ -495,10 +494,6 @@ 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)

View File

@ -1480,7 +1480,6 @@ namespace BTCPayServer.Tests
[Fact]
[Trait("Lightning", "Lightning")]
[Trait("Integration", "Integration")]
public async Task CanSetPaymentMethodLimits()
{
using var tester = CreateServerTester();
@ -1516,10 +1515,9 @@ namespace BTCPayServer.Tests
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
// 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());
Assert.Single(invoice.CryptoInfo);
Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType);
// 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
@ -2274,18 +2272,18 @@ namespace BTCPayServer.Tests
});
// Test on the webhooks
await user.AssertHasWebhookEvent<WebhookInvoiceSettledEvent>(WebhookEventType.InvoiceSettled,
user.AssertHasWebhookEvent<WebhookInvoiceSettledEvent>(WebhookEventType.InvoiceSettled,
c =>
{
Assert.False(c.ManuallyMarked);
Assert.True(c.OverPaid);
});
await user.AssertHasWebhookEvent<WebhookInvoiceProcessingEvent>(WebhookEventType.InvoiceProcessing,
user.AssertHasWebhookEvent<WebhookInvoiceProcessingEvent>(WebhookEventType.InvoiceProcessing,
c =>
{
Assert.True(c.OverPaid);
});
await user.AssertHasWebhookEvent<WebhookInvoiceReceivedPaymentEvent>(WebhookEventType.InvoiceReceivedPayment,
user.AssertHasWebhookEvent<WebhookInvoiceReceivedPaymentEvent>(WebhookEventType.InvoiceReceivedPayment,
c =>
{
Assert.False(c.AfterExpiration);
@ -2295,7 +2293,7 @@ namespace BTCPayServer.Tests
Assert.StartsWith(txId.ToString(), c.Payment.Id);
});
await user.AssertHasWebhookEvent<WebhookInvoicePaymentSettledEvent>(WebhookEventType.InvoicePaymentSettled,
user.AssertHasWebhookEvent<WebhookInvoicePaymentSettledEvent>(WebhookEventType.InvoicePaymentSettled,
c =>
{
Assert.False(c.AfterExpiration);
@ -2782,7 +2780,7 @@ namespace BTCPayServer.Tests
await tester.StartAsync();
var acc = tester.NewAccount();
await acc.GrantAccessAsync(true);
acc.GrantAccess(true);
var settings = tester.PayTester.GetService<SettingsRepository>();
var emailSenderFactory = tester.PayTester.GetService<EmailSenderFactory>();
@ -2807,7 +2805,7 @@ 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",
@ -2985,10 +2983,10 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Integration", "Integration")]
[Trait("Selenium", "Selenium")]
public async Task CanCreateReports()
{
using var tester = CreateServerTester(newDb: true);
using var tester = CreateServerTester();
tester.ActivateLightning();
tester.DeleteStore = false;
await tester.StartAsync();
@ -3086,55 +3084,6 @@ 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
var addr = await tester.ExplorerNode.GetNewAddressAsync();
// Two invoices get refunded
for (int i = 0; i < 2; i++)
{
var inv = await client.CreateInvoice(acc.StoreId, new CreateInvoiceRequest() { Amount = 10m, Currency = "USD" });
await acc.PayInvoice(inv.Id);
await client.MarkInvoiceStatus(acc.StoreId, inv.Id, new MarkInvoiceStatusRequest() { Status = InvoiceStatus.Settled });
var refund = await client.RefundInvoice(acc.StoreId, inv.Id, new RefundInvoiceRequest() { RefundVariant = RefundVariant.Fiat, PaymentMethod = "BTC" });
async Task AssertData(string currency, decimal awaiting, decimal limit, decimal completed, bool fullyPaid)
{
report = await GetReport(acc, new() { ViewName = "Refunds" });
var currencyIndex = report.GetIndex("Currency");
var awaitingIndex = report.GetIndex("Awaiting");
var fullyPaidIndex = report.GetIndex("FullyPaid");
var completedIndex = report.GetIndex("Completed");
var limitIndex = report.GetIndex("Limit");
var d = Assert.Single(report.Data.Where(d => d[report.GetIndex("InvoiceId")].Value<string>() == inv.Id));
Assert.Equal(fullyPaid, (bool)d[fullyPaidIndex]);
Assert.Equal(currency, d[currencyIndex].Value<string>());
Assert.Equal(completed, (((JObject)d[completedIndex])["v"]).Value<decimal>());
Assert.Equal(awaiting, (((JObject)d[awaitingIndex])["v"]).Value<decimal>());
Assert.Equal(limit, (((JObject)d[limitIndex])["v"]).Value<decimal>());
}
await AssertData("USD", awaiting: 0.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
var payout = await client.CreatePayout(refund.Id, new CreatePayoutRequest() { Destination = addr.ToString(), PaymentMethod = "BTC" });
await AssertData("USD", awaiting: 10.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
await client.ApprovePayout(acc.StoreId, payout.Id, new ApprovePayoutRequest());
await AssertData("USD", awaiting: 10.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
if (i == 0)
{
await client.MarkPayoutPaid(acc.StoreId, payout.Id);
await AssertData("USD", awaiting: 0.0m, limit: 10.0m, completed: 10.0m, fullyPaid: true);
}
if (i == 1)
{
await client.CancelPayout(acc.StoreId, payout.Id);
await AssertData("USD", awaiting: 0.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
}
}
}
private async Task<StoreReportResponse> GetReport(TestAccount acc, StoreReportRequest req)

View File

@ -18,6 +18,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.DevTools.V100.Network;
using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;

View File

@ -89,7 +89,7 @@ services:
- merchant_lnd
selenium:
image: selenium/standalone-chrome:125.0
image: selenium/standalone-chrome:101.0
extra_hosts:
- "tests:172.23.0.18"
expose:
@ -99,7 +99,7 @@ services:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.5.4
image: nicolasdorier/nbxplorer:2.5.0
restart: unless-stopped
ports:
- "32838:32838"
@ -163,7 +163,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v24.05
image: btcpayserver/lightning:v23.08-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -171,7 +171,6 @@ services:
LIGHTNINGD_CHAIN: "btc"
LIGHTNINGD_NETWORK: "regtest"
LIGHTNINGD_OPT: |
developer
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
announce-addr=customer_lightningd:9735
@ -191,15 +190,13 @@ services:
- bitcoind
merchant_lightningd:
image: btcpayserver/lightning:v24.05
image: btcpayserver/lightning:v23.08-dev
stop_signal: SIGKILL
restart: unless-stopped
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
@ -217,7 +214,6 @@ services:
- "merchant_lightningd_datadir:/root/.lightning"
depends_on:
- bitcoind
postgres:
image: postgres:13.4
environment:
@ -228,7 +224,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.18.1-beta
image: btcpayserver/lnd:v0.17.4-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -263,7 +259,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.18.1-beta
image: btcpayserver/lnd:v0.17.4-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -310,29 +306,27 @@ services:
- "tor_datadir:/home/tor/.tor"
- "torrcdir:/usr/local/etc/tor"
- "tor_servicesdir:/var/lib/tor/hidden_services"
monerod:
image: btcpayserver/monero:0.18.3.3
restart: unless-stopped
container_name: xmr_monerod
entrypoint: monerod --fixed-difficulty 200 --rpc-bind-ip=0.0.0.0 --confirm-external-bind --rpc-bind-port=18081 --block-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/block?cryptoCode=xmr&hash=%s" --testnet --no-igd --hide-my-port --offline --non-interactive
volumes:
- "monero_data:/home/monero/.bitmonero"
ports:
- "18081:18081"
image: btcpayserver/monero:0.18.2.2-5
restart: unless-stopped
container_name: xmr_monerod
entrypoint: sleep 999999
# entrypoint: monerod --fixed-difficulty 200 --rpc-bind-ip=0.0.0.0 --confirm-external-bind --rpc-bind-port=18081 --block-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/block?cryptoCode=xmr&hash=%s" --testnet --no-igd --hide-my-port --offline
volumes:
- "monero_data:/home/monero/.bitmonero"
ports:
- "18081:18081"
monero_wallet:
image: btcpayserver/monero:0.18.3.3
image: btcpayserver/monero:0.18.2.2-5
restart: unless-stopped
container_name: xmr_wallet_rpc
entrypoint: monero-wallet-rpc --testnet --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-dir=/wallet --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s"
entrypoint: monero-wallet-rpc --testnet --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-file=/wallet/wallet.keys --password-file=/wallet/password --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s"
ports:
- "18082:18082"
volumes:
- "./monero_wallet:/wallet"
depends_on:
- monerod
- monerod
litecoind:
restart: unless-stopped
image: btcpayserver/litecoin:0.18.1

View File

@ -86,7 +86,7 @@ services:
- merchant_lnd
selenium:
image: selenium/standalone-chrome:125.0
image: selenium/standalone-chrome:101.0
extra_hosts:
- "tests:172.23.0.18"
expose:
@ -96,7 +96,7 @@ services:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.5.4
image: nicolasdorier/nbxplorer:2.5.0
restart: unless-stopped
ports:
- "32838:32838"
@ -149,7 +149,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v24.05
image: btcpayserver/lightning:v23.08-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -157,7 +157,6 @@ services:
LIGHTNINGD_CHAIN: "btc"
LIGHTNINGD_NETWORK: "regtest"
LIGHTNINGD_OPT: |
developer
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
announce-addr=customer_lightningd:9735
@ -177,15 +176,13 @@ services:
- bitcoind
merchant_lightningd:
image: btcpayserver/lightning:v24.05
image: btcpayserver/lightning:v23.08-dev
stop_signal: SIGKILL
restart: unless-stopped
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
@ -214,7 +211,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.18.1-beta
image: btcpayserver/lnd:v0.17.4-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -251,7 +248,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.18.1-beta
image: btcpayserver/lnd:v0.17.4-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"

View File

@ -1,199 +0,0 @@
#!/bin/bash
USERHOST="btcpay.local"
BASE="https://localhost:14142"
API_BASE="$BASE/api/v1"
PASSWORD="rockstar"
# Ensure we are in the script directory
cd "$(dirname "${BASH_SOURCE}")"
# Create admin user
admin_id=$(curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'email': 'admin@$USERHOST', 'password': '$PASSWORD', 'isAdministrator': true }" \
"$API_BASE/users" | jq -r '.id')
printf "Admin ID: %s\n" "$admin_id"
# Create unlimited access API key
admin_api_key=$(curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'permissions': ['unrestricted'], 'label': 'Unrestricted' }" \
--user "admin@$USERHOST:$PASSWORD" \
"$API_BASE/api-keys" | jq -r '.apiKey')
printf "Admin API Key: %s\n" "$admin_api_key"
printf "\n"
# Create Store Owner
owner_id=$(curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'email': 'owner@$USERHOST', 'password': '$PASSWORD', 'isAdministrator': false }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/users" | jq -r '.id')
printf "Store Owner ID: %s\n" "$owner_id"
# Create Store Manager
manager_id=$(curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'email': 'manager@$USERHOST', 'password': '$PASSWORD', 'isAdministrator': false }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/users" | jq -r '.id')
printf "Store Manager ID: %s\n" "$manager_id"
# Create Store Employee
employee_id=$(curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'email': 'employee@$USERHOST', 'password': '$PASSWORD', 'isAdministrator': false }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/users" | jq -r '.id')
printf "Store Employee ID: %s\n" "$employee_id"
printf "\n"
# Create Satoshis Steaks store
res=$(curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'name': 'Satoshis Steaks', 'checkoutType': 'V2', 'lightningAmountInSatoshi': true, 'onChainWithLnInvoiceFallback': true, 'playSoundOnPayment': true, 'defaultCurrency': 'EUR' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores")
store_id_satoshis_steaks=$( echo $res | jq -r '.id')
if [ -z "${store_id_satoshis_steaks}" ]; then
printf "Error creating Satoshis Steaks store: %s\n" "$res"
exit 1
fi
printf "Satoshis Steaks Store ID: %s\n" "$store_id_satoshis_steaks"
# Create Hot Wallet for Satoshis Steaks store
wallet_enabled_satoshis_steaks=$(curl -s -k -X PUT -H 'Content-Type: application/json' \
-d "{'enabled': true, 'config': 'tpubDC2mCtL7EPhey3qRgHXmKQRraxXgiuSTkHdJbDW22xLK1YMXy8jdEq7jx2UN5z1wU5xBWWZdSpAobG1bbZBTR4f8R3AjL31EzoexpngKUXM' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_satoshis_steaks/payment-methods/BTC-CHAIN")
# Create Internal Node connection for Satoshis Steaks store
ln_enabled_satoshis_steaks=$(curl -s -k -X PUT -H 'Content-Type: application/json' \
-d "{'enabled': true, 'config': { 'connectionString': 'Internal Node' } }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_satoshis_steaks/payment-methods/BTC-LN")
# LNURL settings
curl -s -k -X PUT -H 'Content-Type: application/json' \
-d "{'enabled': true, 'config': { 'lud12Enabled': true, 'useBech32Scheme': true } }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_satoshis_steaks/payment-methods/BTC-LNURL" >/dev/null 2>&1
# Fund Satoshis Steaks wallet
btcaddress_satoshis_steaks=$(curl -s -k -X GET -H 'Content-Type: application/json' \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_satoshis_steaks/payment-methods/onchain/BTC/wallet/address" | jq -r '.address')
./docker-bitcoin-cli.sh sendtoaddress "$btcaddress_satoshis_steaks" 6.15 >/dev/null 2>&1
printf "\n"
# Add store users to Satoshis Steaks store
curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'userId': '$owner_id', 'role': 'Owner' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_satoshis_steaks/users" >/dev/null 2>&1
curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'userId': '$manager_id', 'role': 'Manager' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_satoshis_steaks/users" >/dev/null 2>&1
curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'userId': '$employee_id', 'role': 'Employee' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_satoshis_steaks/users" >/dev/null 2>&1
# Create Nakamoto Nuggets store
store_id_nakamoto_nuggets=$(curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'name': 'Nakamoto Nuggets', 'checkoutType': 'V2', 'lightningAmountInSatoshi': true, 'onChainWithLnInvoiceFallback': true, 'playSoundOnPayment': true }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores" | jq -r '.id')
printf "Nakamoto Nuggets Store ID: %s\n" "$store_id_nakamoto_nuggets"
# Create Hot Wallet for Nakamoto Nuggets store
# Seed: "resist camera spread better amazing cliff giraffe duty betray throw twelve father"
wallet_enabled_nakamoto_nuggets=$(curl -s -k -X PUT -H 'Content-Type: application/json' \
-d "{'enabled': true, 'config': 'tpubDD79XF4pzhmPSJ9AyUay9YbXAeD1c6nkUqC32pnKARJH6Ja5hGUfGc76V82ahXpsKqN6UcSGXMkzR34aZq4W23C6DAdZFaVrzWqzj24F8BC' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_nakamoto_nuggets/payment-methods/BTC-CHAIN")
# Connect Nakamoto Nuggets with Merchant LND Lightning node
curl -s -k -X PUT -H 'Content-Type: application/json' \
-d "{'enabled': true, 'config': { 'connectionString': 'type=lnd-rest;server=http://lnd:lnd@127.0.0.1:35531/;allowinsecure=true' }}" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_nakamoto_nuggets/payment-methods/BTC-LN" >/dev/null 2>&1
# LNURL settings
curl -s -k -X PUT -H 'Content-Type: application/json' \
-d "{'enabled': true, 'config': { 'lud12Enabled': true, 'useBech32Scheme': true } }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_nakamoto_nuggets/payment-methods/BTC-LNURL" >/dev/null 2>&1
# Add store users to Nakamoto Nuggets store
curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'userId': '$owner_id', 'role': 'Owner' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_nakamoto_nuggets/users" >/dev/null 2>&1
curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'userId': '$manager_id', 'role': 'Manager' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_nakamoto_nuggets/users" >/dev/null 2>&1
curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'userId': '$employee_id', 'role': 'Employee' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_nakamoto_nuggets/users" >/dev/null 2>&1
# Create Nakamoto Nuggets keypad app
keypad_app_id_nakamoto_nuggets=$(curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'appName': 'Keypad', 'title': 'Keypad', 'defaultView': 'light', 'currency': 'SATS' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/{$store_id_nakamoto_nuggets}/apps/pos" | jq -r '.id')
printf "Nakamoto Nuggets Keypad POS ID: %s\n" "$keypad_app_id_nakamoto_nuggets"
# Create Nakamoto Nuggets cart app
cart_app_id_nakamoto_nuggets=$(curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'appName': 'Cart', 'title': 'Cart', 'defaultView': 'cart', 'template': '[{\"id\":\"birell beer\",\"image\":\"https://i.imgur.com/r8N6rTU.png\",\"priceType\":\"Fixed\",\"price\":\"20\",\"title\":\"Birell Beer\",\"disabled\":false},{\"id\":\"flavoured birell beer\",\"image\":\"https://i.imgur.com/de43iUd.png\",\"priceType\":\"Fixed\",\"price\":\"20\",\"title\":\"Flavoured Birell Beer\",\"disabled\":false},{\"id\":\"wostok\",\"image\":\"https://i.imgur.com/gP6zqub.png\",\"priceType\":\"Fixed\",\"price\":\"25\",\"title\":\"Wostok\",\"disabled\":false},{\"id\":\"pilsner beer\",\"image\":\"https://i.imgur.com/M4EEaEP.png\",\"priceType\":\"Fixed\",\"price\":\"30\",\"title\":\"Pilsner Beer\",\"disabled\":false},{\"id\":\"club mate\",\"image\":\"https://i.imgur.com/H9p9Xwc.png\",\"priceType\":\"Fixed\",\"price\":\"35\",\"title\":\"Club Mate\",\"disabled\":false},{\"id\":\"seicha / selo / koka\",\"image\":\"https://i.imgur.com/ReW3RKe.png\",\"priceType\":\"Fixed\",\"price\":\"35\",\"title\":\"Seicha / Selo / Koka\",\"disabled\":false},{\"id\":\"limonada z kopanic\",\"image\":\"https://i.imgur.com/2Xb35Zs.png\",\"priceType\":\"Fixed\",\"price\":\"40\",\"title\":\"Limonada z Kopanic\",\"disabled\":false},{\"id\":\"mellow drink\",\"image\":\"https://i.imgur.com/ilDUWiP.png\",\"priceType\":\"Fixed\",\"price\":\"40\",\"title\":\"Mellow Drink\",\"disabled\":false},{\"id\":\"bacilli drink\",\"image\":\"https://i.imgur.com/3BsCLgG.png\",\"priceType\":\"Fixed\",\"price\":\"40\",\"title\":\"Bacilli Drink\",\"disabled\":false},{\"description\":\"\",\"id\":\"vincentka\",\"image\":\"https://i.imgur.com/99reAEg.png\",\"priceType\":\"Fixed\",\"price\":\"20\",\"title\":\"Vincentka\",\"disabled\":false,\"index\":\"-1\"},{\"id\":\"kinder bar\",\"image\":\"https://i.imgur.com/va9i6SQ.png\",\"priceType\":\"Fixed\",\"price\":\"20\",\"title\":\"Kinder bar\",\"disabled\":false},{\"id\":\"nutrend bar\",\"image\":\"https://i.imgur.com/zzdIup0.png\",\"priceType\":\"Fixed\",\"price\":\"15\",\"title\":\"Nutrend bar\",\"disabled\":false},{\"id\":\"yoghurt\",\"image\":\"https://i.imgur.com/biP4Dr8.png\",\"priceType\":\"Fixed\",\"price\":\"20\",\"title\":\"Yoghurt\",\"disabled\":false},{\"id\":\"mini magnum\",\"image\":\"https://i.imgur.com/tveN4Aa.png\",\"priceType\":\"Fixed\",\"price\":\"35\",\"title\":\"Mini Magnum\",\"disabled\":false},{\"description\":\"\",\"id\":\"nanuk do:pusy\",\"image\":\"https://i.imgur.com/EzZN6lV.png\",\"priceType\":\"Fixed\",\"price\":\"30\",\"title\":\"Nanuk DO:PUSY\",\"disabled\":false,\"index\":\"-1\"},{\"id\":\"alpro dessert\",\"image\":\"https://i.imgur.com/L0MHkcs.png\",\"priceType\":\"Fixed\",\"price\":\"30\",\"title\":\"Alpro dessert\",\"disabled\":false},{\"id\":\"mixitka bar\",\"image\":\"https://i.imgur.com/gHuTGK3.png\",\"priceType\":\"Fixed\",\"price\":\"30\",\"title\":\"Mixitka bar\",\"disabled\":false},{\"id\":\"instatni polivka\",\"image\":\"https://cdn.rohlik.cz/images/grocery/products/722313/722313-1598298944.jpg\",\"priceType\":\"Fixed\",\"price\":\"15\",\"title\":\"Instatni polivka\",\"disabled\":false},{\"id\":\"m&amp;s instatni polivka\",\"image\":\"https://i.imgur.com/Y8LCJbG.png\",\"priceType\":\"Fixed\",\"price\":\"60\",\"title\":\"M&amp;S instatni polivka\",\"disabled\":false}]' }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/{$store_id_nakamoto_nuggets}/apps/pos" | jq -r '.id')
printf "Nakamoto Nuggets Cart POS ID: %s\n" "$cart_app_id_nakamoto_nuggets"
# Fund Nakamoto Nuggets wallet
btcaddress_nakamoto_nuggets=$(curl -s -k -X GET -H 'Content-Type: application/json' \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_nakamoto_nuggets/payment-methods/onchain/BTC/wallet/address" | jq -r '.address')
./docker-bitcoin-cli.sh sendtoaddress "$btcaddress_nakamoto_nuggets" 6.15 >/dev/null 2>&1
printf "\n"
# Create External Lightning based store
store_id_externalln=$(curl -s -k -X POST -H 'Content-Type: application/json' \
-d "{'name': 'External Lightning (LND)', 'checkoutType': 'V2', 'lightningAmountInSatoshi': true, 'onChainWithLnInvoiceFallback': true }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores" | jq -r '.id')
printf "External Lightning Store ID: %s\n" "$store_id_externalln"
# Connect External Lightning based store with Customer LND Lightning node
curl -s -k -X PUT -H 'Content-Type: application/json' \
-d "{'enabled': true, 'config': { 'connectionString': 'type=lnd-rest;server=http://lnd:lnd@127.0.0.1:35532/;allowinsecure=true' } }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_externalln/payment-methods/BTC-LN" >/dev/null 2>&1
# LNURL settings
curl -s -k -X PUT -H 'Content-Type: application/json' \
-d "{'enabled': true, 'config': { 'lud12Enabled': true, 'useBech32Scheme': true } }" \
-H "Authorization: token $admin_api_key" \
"$API_BASE/stores/$store_id_externalln/payment-methods/BTC-LNURL" >/dev/null 2>&1
printf "\n"
# Mine some blocks
./docker-bitcoin-generate.sh 5 >/dev/null 2>&1

View File

@ -46,13 +46,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.23" />
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.22" />
<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.6.0" />
<PackageReference Include="CsvHelper" Version="32.0.3" />
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.5.4" />
<PackageReference Include="CsvHelper" Version="15.0.5" />
<PackageReference Include="Dapper" Version="2.1.28" />
<PackageReference Include="Fido2" Version="2.0.2" />
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
<PackageReference Include="LNURL" Version="0.0.34" />
@ -77,8 +77,8 @@
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.1" />
</ItemGroup>
<ItemGroup>

View File

@ -26,16 +26,16 @@
{
@if (Model.Store != null)
{
<div class="accordion-item" permission="@Policies.CanViewStoreSettings">
<div class="accordion-item" permission="@Policies.CanModifyStoreSettings">
<div class="accordion-body">
<ul class="navbar-nav">
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
<li class="nav-item">
<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" permission="@Policies.CanViewStoreSettings">
<li class="nav-item">
<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,6 +115,7 @@
</a>
</li>
}
</ul>
</div>
</div>
@ -296,15 +297,6 @@
</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>

View File

@ -29,8 +29,6 @@ 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,
@ -40,7 +38,6 @@ namespace BTCPayServer.Components.MainNav
UserManager<ApplicationUser> userManager,
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
CustodianAccountRepository custodianAccountRepository,
SettingsRepository settingsRepository,
PoliciesSettings policiesSettings)
{
_storeRepo = storeRepo;
@ -50,19 +47,13 @@ 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 serverSettings = await _settingsRepository.GetSettingAsync<ServerSettings>() ?? new ServerSettings();
var vm = new MainNavViewModel
{
Store = store,
ContactUrl = serverSettings.ContactUrl
};
var vm = new MainNavViewModel { Store = store };
#if ALTCOINS
vm.AltcoinsBuild = true;
#endif
@ -101,5 +92,7 @@ namespace BTCPayServer.Components.MainNav
}
private string UserId => _userManager.GetUserId(HttpContext.User);
public PoliciesSettings PoliciesSettings { get; }
}
}

View File

@ -13,7 +13,6 @@ 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

View File

@ -34,36 +34,34 @@
}
else if (Model.Invoices.Any())
{
<div class="table-responsive mt-3 mb-0">
<table class="table table-hover mb-0">
<thead>
<table class="table table-hover mt-3 mb-0">
<thead>
<tr>
<th class="w-125px">Date</th>
<th class="text-nowrap">Invoice Id</th>
<th>Status</th>
<th class="text-end">Amount</th>
</tr>
</thead>
<tbody>
@foreach (var invoice in Model.Invoices)
{
<tr>
<th class="w-125px">Date</th>
<th class="text-nowrap">Invoice Id</th>
<th>Status</th>
<th class="text-end">Amount</th>
<td>@invoice.Date.ToTimeAgo()</td>
<td>
<a asp-controller="UIInvoice" asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId" class="text-break">@invoice.InvoiceId</a>
</td>
<td>
<vc:invoice-status state="invoice.Status" payments="invoice.Details.Payments" invoice-id="@invoice.InvoiceId"
is-archived="invoice.Details.Archived" has-refund="invoice.HasRefund" />
</td>
<td class="text-end">
<span data-sensitive>@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</span>
</td>
</tr>
</thead>
<tbody>
@foreach (var invoice in Model.Invoices)
{
<tr>
<td>@invoice.Date.ToTimeAgo()</td>
<td>
<a asp-controller="UIInvoice" asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId" class="text-break">@invoice.InvoiceId</a>
</td>
<td>
<vc:invoice-status state="invoice.Status" payments="invoice.Details.Payments" invoice-id="@invoice.InvoiceId"
is-archived="invoice.Details.Archived" has-refund="invoice.HasRefund" />
</td>
<td class="text-end">
<span data-sensitive>@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</span>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</tbody>
</table>
}
else
{

View File

@ -31,63 +31,61 @@
}
else if (Model.Transactions.Any())
{
<div class="table-responsive mt-3 mb-0">
<table class="table table-hover mb-0">
<thead>
<table class="table table-hover mt-3 mb-0">
<thead>
<tr>
<th class="w-125px">Date</th>
<th>Transaction</th>
<th>Labels</th>
<th class="text-end">Amount</th>
</tr>
</thead>
<tbody>
@foreach (var tx in Model.Transactions)
{
<tr>
<th class="w-125px">Date</th>
<th>Transaction</th>
<th>Labels</th>
<th class="text-end">Amount</th>
<td>@tx.Timestamp.ToTimeAgo()</td>
<td>
<vc:truncate-center text="@tx.Id" link="@tx.Link" classes="truncate-center-id" />
</td>
<td>
@if (tx.Labels.Any())
{
<div class="d-flex flex-wrap gap-2 align-items-center">
@foreach (var label in tx.Labels)
{
<div class="transaction-label" style="--label-bg:@label.Color;--label-fg:@label.TextColor">
<span>@label.Text</span>
@if (!string.IsNullOrEmpty(label.Link))
{
<a class="transaction-label-info transaction-details-icon" href="@label.Link"
target="_blank" rel="noreferrer noopener" title="@label.Tooltip" data-bs-html="true"
data-bs-toggle="tooltip" data-bs-custom-class="transaction-label-tooltip">
<vc:icon symbol="info" />
</a>
}
</div>
}
</div>
}
</td>
@if (tx.Positive)
{
<td class="text-end text-success">
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
</td>
}
else
{
<td class="text-end text-danger">
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
</td>
}
</tr>
</thead>
<tbody>
@foreach (var tx in Model.Transactions)
{
<tr>
<td>@tx.Timestamp.ToTimeAgo()</td>
<td>
<vc:truncate-center text="@tx.Id" link="@tx.Link" classes="truncate-center-id" />
</td>
<td>
@if (tx.Labels.Any())
{
<div class="d-flex flex-wrap gap-2 align-items-center">
@foreach (var label in tx.Labels)
{
<div class="transaction-label" style="--label-bg:@label.Color;--label-fg:@label.TextColor">
<span>@label.Text</span>
@if (!string.IsNullOrEmpty(label.Link))
{
<a class="transaction-label-info transaction-details-icon" href="@label.Link"
target="_blank" rel="noreferrer noopener" title="@label.Tooltip" data-bs-html="true"
data-bs-toggle="tooltip" data-bs-custom-class="transaction-label-tooltip">
<vc:icon symbol="info" />
</a>
}
</div>
}
</div>
}
</td>
@if (tx.Positive)
{
<td class="text-end text-success">
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
</td>
}
else
{
<td class="text-end text-danger">
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
</td>
}
</tr>
}
</tbody>
</table>
</div>
}
</tbody>
</table>
}
else
{

View File

@ -4,23 +4,23 @@
var isTruncated = !string.IsNullOrEmpty(Model.Start) && !string.IsNullOrEmpty(Model.End);
@if (Model.Copy) classes += " truncate-center--copy";
@if (Model.Elastic) classes += " truncate-center--elastic";
var prefix = Model.IsVue ? ":" : "";
}
<span class="truncate-center @classes" id="@Model.Id" data-text="@Model.Text">
<span class="truncate-center @classes"@(!string.IsNullOrEmpty(Model.Id) ? $"id={Model.Id}" : null) data-text=@Safe.Json(Model.Text)>
@if (Model.IsVue)
{
<span class="truncate-center-truncated" data-bs-toggle="tooltip" :title="@Model.Text">
<span class="truncate-center-truncated" data-bs-toggle="tooltip" :title=@Safe.Json(Model.Text)>
@if (Model.Elastic)
{
<span class="truncate-center-start" v-text="@Model.Text"></span>
<span class="truncate-center-start" v-text=@Safe.Json(Model.Text)></span>
}
else
{
<span class="truncate-center-start" v-text="@(Model.Text).length > 2 * @(Model.Padding) ? (@(Model.Text).slice(0, @(Model.Padding)) + '…') : @(Model.Text)"></span>
<span class="truncate-center-start" v-text=@Safe.Json($"{Model.Text}.slice(0, {Model.Padding})")></span>
<span>…</span>
}
<span class="truncate-center-end" v-text="@(Model.Text).slice(-@(Model.Padding))" v-if="@(Model.Text).length > 2 * @(Model.Padding)"></span>
<span class="truncate-center-end" v-text=@Safe.Json($"{Model.Text}.slice(-{Model.Padding})")></span>
</span>
<span class="truncate-center-text" v-text="@Model.Text"></span>
<span class="truncate-center-text" v-text=@Safe.Json(Model.Text)></span>
}
else
{
@ -35,13 +35,13 @@
}
@if (Model.Copy)
{
<button type="button" class="btn btn-link p-0" @(prefix)data-clipboard="@Model.Text">
<button type="button" class="btn btn-link p-0" @(Model.IsVue ? ":" : string.Empty)data-clipboard=@Safe.Json(Model.Text)>
<vc:icon symbol="copy" />
</button>
}
@if (!string.IsNullOrEmpty(Model.Link))
{
<a @(prefix)href="@Model.Link" rel="noreferrer noopener" target="_blank">
<a @(Model.IsVue ? ":" : "")href="@Model.Link" rel="noreferrer noopener" target="_blank">
<vc:icon symbol="info" />
</a>
}

View File

@ -213,7 +213,6 @@ namespace BTCPayServer.Controllers.Greenfield
}
[HttpDelete("~/api/v1/apps/{appId}")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> DeleteApp(string appId)
{
var app = await _appService.GetApp(appId, null, includeArchived: true);
@ -239,7 +238,7 @@ namespace BTCPayServer.Controllers.Greenfield
return new CrowdfundSettings
{
Title = request.Title?.Trim() ?? request.AppName,
Title = request.Title?.Trim(),
Enabled = request.Enabled ?? true,
EnforceTargetAmount = request.EnforceTargetAmount ?? false,
StartDate = request.StartDate?.UtcDateTime,
@ -273,9 +272,8 @@ namespace BTCPayServer.Controllers.Greenfield
{
return new PointOfSaleSettings
{
Title = request.Title ?? request.AppName,
Title = request.Title,
DefaultView = (PosViewType)request.DefaultView,
ShowItems = request.ShowItems,
ShowCustomAmount = request.ShowCustomAmount,
ShowDiscount = request.ShowDiscount,
ShowSearch = request.ShowSearch,
@ -337,7 +335,6 @@ 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,

View File

@ -326,7 +326,7 @@ namespace BTCPayServer.Controllers.Greenfield
if (result == null)
{
return this.CreateAPIError(404, "trade-not-found",
$"Could not find the trade with ID {tradeId} on {custodianAccount.Name}");
$"Could not find the the trade with ID {tradeId} on {custodianAccount.Name}");
}
return Ok(ToModel(result, accountId, custodianAccount.CustodianCode));
}

View File

@ -401,14 +401,6 @@ namespace BTCPayServer.Controllers.Greenfield
var accounting = invoicePaymentMethod.Calculate();
var cryptoPaid = accounting.Paid;
var dueAmount = accounting.TotalDue;
// If no payment, but settled and marked, assume it has been fully paid
if (cryptoPaid is 0 && invoice is { Status: InvoiceStatusLegacy.Confirmed or InvoiceStatusLegacy.Complete, ExceptionStatus: InvoiceExceptionStatus.Marked })
{
cryptoPaid = accounting.TotalDue;
dueAmount = 0;
}
var cdCurrency = _currencyNameTable.GetCurrencyData(invoice.Currency, true);
var paidCurrency = Math.Round(cryptoPaid * invoicePaymentMethod.Rate, cdCurrency.Divisibility);
var rateResult = await _rateProvider.FetchRate(
@ -476,6 +468,7 @@ namespace BTCPayServer.Controllers.Greenfield
return this.CreateValidationError(ModelState);
}
var dueAmount = accounting.TotalDue;
createPullPayment.Currency = cryptoCode;
createPullPayment.Amount = Math.Round(paidAmount - dueAmount, appliedDivisibility);
createPullPayment.AutoApproveClaims = true;

View File

@ -1,8 +1,6 @@
#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;
@ -24,9 +22,7 @@ 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 MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
@ -157,7 +153,20 @@ namespace BTCPayServer.Controllers.Greenfield
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
var ppId = await _pullPaymentService.CreatePullPayment(storeId, request);
var ppId = await _pullPaymentService.CreatePullPayment(new CreatePullPayment()
{
StartsAt = request.StartsAt,
ExpiresAt = request.ExpiresAt,
Period = request.Period,
BOLT11Expiration = request.BOLT11Expiration,
Name = request.Name,
Description = request.Description,
Amount = request.Amount,
Currency = request.Currency,
StoreId = storeId,
PaymentMethodIds = paymentMethods,
AutoApproveClaims = request.AutoApproveClaims
});
var pp = await _pullPaymentService.GetPullPayment(ppId, false);
return this.Ok(CreatePullPaymentData(pp));
}
@ -191,39 +200,13 @@ namespace BTCPayServer.Controllers.Greenfield
[HttpPost]
[Route("~/api/v1/pull-payments/{pullPaymentId}/boltcards")]
[AllowAnonymous]
public async Task<IActionResult> RegisterBoltcard(string pullPaymentId, RegisterBoltcardRequest request, string? onExisting = null)
public async Task<IActionResult> RegisterBoltcard(string pullPaymentId, RegisterBoltcardRequest request)
{
if (pullPaymentId is null)
return PullPaymentNotFound();
var pp = await _pullPaymentService.GetPullPayment(pullPaymentId, false);
if (pp is null)
return PullPaymentNotFound();
var issuerKey = await _settingsRepository.GetIssuerKey(_env);
BoltcardPICCData? picc = null;
// 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 o)
{
ModelState.AddModelError(nameof(request.LNURLW), "The LNURLW 'p=' parameter cannot be decrypted");
return this.CreateValidationError(ModelState);
}
picc = o;
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");
@ -234,68 +217,15 @@ namespace BTCPayServer.Controllers.Greenfield
return this.CreateAPIError(400, "lnurl-not-supported", "This pull payment currency should be BTC or SATS and accept lightning");
}
// 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
};
BoltcardKeys keys;
int version;
var registration = await _dbContextFactory.GetBoltcardRegistration(issuerKey, request.UID);
if (request.OnExisting is OnExistingBehavior.UpdateVersion ||
(request.OnExisting is null && registration?.PullPaymentId is null))
{
version = await _dbContextFactory.LinkBoltcardToPullPayment(pullPaymentId, issuerKey, request.UID, request.OnExisting);
keys = issuerKey.CreatePullPaymentCardKey(request.UID, version, pullPaymentId).DeriveBoltcardKeys(issuerKey);
}
else
{
if (registration?.PullPaymentId is null)
{
ModelState.AddModelError(nameof(request.UID), "This card isn't registered");
return this.CreateValidationError(ModelState);
}
if (pullPaymentId != registration.PullPaymentId)
{
ModelState.AddModelError(nameof(request.UID), "This card is registered on a different pull payment");
return this.CreateValidationError(ModelState);
}
var ppId = registration.PullPaymentId;
version = registration.Version;
int retryCount = 0;
retry:
keys = issuerKey.CreatePullPaymentCardKey(request!.UID, version, ppId).DeriveBoltcardKeys(issuerKey);
// The server version may be higher than the card.
// If that is the case, let's try a few versions until we find the right one
// by checking c.
if (request?.LNURLW is { } lnurlw &&
ExtractC(lnurlw) is string c &&
picc is not null)
{
if (!keys.AuthenticationKey.CheckSunMac(c, picc))
{
retryCount++;
version--;
if (version < 0 || retryCount > 5)
{
ModelState.AddModelError(nameof(request.UID), "Unable to get keys of this card, it might be caused by a version mismatch");
return this.CreateValidationError(ModelState);
}
goto retry;
}
}
}
var issuerKey = await _settingsRepository.GetIssuerKey(_env);
var version = await _dbContextFactory.LinkBoltcardToPullPayment(pullPaymentId, issuerKey, request.UID, request.OnExisting);
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://");
var resp = new RegisterBoltcardResponse()
return Ok(new RegisterBoltcardResponse()
{
LNURLW = boltcardUrl,
Version = version,
@ -304,26 +234,9 @@ retry:
K2 = Encoders.Hex.EncodeData(keys.AuthenticationKey.ToBytes()).ToUpperInvariant(),
K3 = Encoders.Hex.EncodeData(keys.K3.ToBytes()).ToUpperInvariant(),
K4 = Encoders.Hex.EncodeData(keys.K4.ToBytes()).ToUpperInvariant(),
};
return Ok(resp);
});
}
public static string? Extract(string? url, string param, int size)
{
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, param + "=([a-f0-9A-F]{" + size + "})");
if (!match.Success)
return null;
return match.Groups[1].Value;
}
public static string? ExtractP(string? url) => Extract(url, "p", 32);
public static string? ExtractC(string? url) => Extract(url, "c", 16);
[HttpGet("~/api/v1/pull-payments/{pullPaymentId}")]
[AllowAnonymous]
public async Task<IActionResult> GetPullPayment(string pullPaymentId)

View File

@ -1,5 +1,4 @@
#nullable enable
using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
@ -9,8 +8,6 @@ using BTCPayServer.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using AuthenticationSchemes = BTCPayServer.Abstractions.Constants.AuthenticationSchemes;
using LightningAddressData = BTCPayServer.Client.Models.LightningAddressData;
@ -34,13 +31,12 @@ namespace BTCPayServer.Controllers.Greenfield
var blob = data.GetBlob();
if (blob is null)
return new LightningAddressData();
return new LightningAddressData
return new LightningAddressData()
{
Username = data.Username,
Max = blob.Max,
Min = blob.Min,
CurrencyCode = blob.CurrencyCode,
InvoiceMetadata = blob.InvoiceMetadata
CurrencyCode = blob.CurrencyCode
};
}
@ -87,17 +83,16 @@ namespace BTCPayServer.Controllers.Greenfield
ModelState.AddModelError(nameof(data.Min), "Minimum must be greater than 0 if provided.");
return this.CreateValidationError(ModelState);
}
if (await _lightningAddressService.Set(new Data.LightningAddressData
if (await _lightningAddressService.Set(new Data.LightningAddressData()
{
StoreDataId = storeId,
Username = username
}.SetBlob(new LightningAddressDataBlob
}.SetBlob(new LightningAddressDataBlob()
{
Max = data.Max,
Min = data.Min,
CurrencyCode = data.CurrencyCode,
InvoiceMetadata = data.InvoiceMetadata
CurrencyCode = data.CurrencyCode
})))
{
return await GetStoreLightningAddress(storeId, username);

View File

@ -24,7 +24,7 @@ namespace BTCPayServer.Controllers.Greenfield
_storeRepository = storeRepository;
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/roles")]
public async Task<IActionResult> GetStoreRoles(string storeId)
{
@ -34,6 +34,7 @@ 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();

View File

@ -27,15 +27,14 @@ namespace BTCPayServer.Controllers.Greenfield
_storeRepository = storeRepository;
_userManager = userManager;
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[Authorize(Policy = Policies.CanModifyStoreSettings, 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)

View File

@ -185,7 +185,7 @@ namespace BTCPayServer.Controllers.Greenfield
RequiresEmailConfirmation = policies.RequiresConfirmedEmail,
RequiresApproval = policies.RequiresUserApproval,
Created = DateTimeOffset.UtcNow,
Approved = isAdmin // auto-approve first admin and users created by an admin
Approved = !anyAdmin && isAdmin // auto-approve first admin
};
var passwordValidation = await this._passwordValidator.ValidateAsync(_userManager, user, request.Password);
if (!passwordValidation.Succeeded)
@ -214,8 +214,7 @@ namespace BTCPayServer.Controllers.Greenfield
return this.CreateValidationError(ModelState);
}
var isNewAdmin = request.IsAdministrator is true;
if (isNewAdmin)
if (request.IsAdministrator is true)
{
if (!anyAdmin)
{
@ -234,21 +233,7 @@ namespace BTCPayServer.Controllers.Greenfield
await _settingsRepository.FirstAdminRegistered(policies, _options.UpdateUrl != null, _options.DisableRegistration, Logs);
}
}
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);
_eventAggregator.Publish(new UserRegisteredEvent() { RequestUri = Request.GetAbsoluteRootUri(), User = user, Admin = request.IsAdministrator is true });
var model = await FromModel(user);
return CreatedAtAction(string.Empty, model);
}

View File

@ -147,7 +147,7 @@ namespace BTCPayServer.Controllers
return await Login(returnUrl);
}
_logger.LogInformation("User {Email} logged in with a login code", user!.Email);
_logger.LogInformation("User with ID {UserId} logged in with a login code", user!.Id);
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 {Email} logged in", user.Email);
_logger.LogInformation("User {UserId} logged in", user.Id);
return RedirectToLocal(returnUrl);
}
if (result.RequiresTwoFactor)
@ -230,7 +230,7 @@ namespace BTCPayServer.Controllers
}
if (result.IsLockedOut)
{
_logger.LogWarning("User {Email} tried to log in, but is locked out", user.Email);
_logger.LogWarning("User {UserId} account locked out", user.Id);
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 {Email} logged in with FIDO2", user.Email);
_logger.LogInformation("User logged in");
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 {Email} logged in with 2FA", user.Email);
_logger.LogInformation("User with ID {UserId} logged in with 2fa", user.Id);
return RedirectToLocal(returnUrl);
}
_logger.LogWarning("User {Email} entered invalid authenticator code", user.Email);
_logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}", user.Id);
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 {Email} logged in with a recovery code", user.Email);
_logger.LogInformation("User with ID {UserId} logged in with a recovery code", user.Id);
return RedirectToLocal(returnUrl);
}
if (result.IsLockedOut)
{
_logger.LogWarning("User {Email} account locked out", user.Email);
_logger.LogWarning("User with ID {UserId} account locked out", user.Id);
return RedirectToAction(nameof(Lockout), new { user.LockoutEnd });
}
_logger.LogWarning("User {Email} entered invalid recovery code", user.Email);
_logger.LogWarning("Invalid recovery code entered for user with ID {UserId}", user.Id);
ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
return View();
}
@ -600,6 +600,7 @@ 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;
}
@ -613,17 +614,15 @@ namespace BTCPayServer.Controllers
RegisteredUserId = user.Id;
TempData[WellKnownTempData.SuccessMessage] = "Account created.";
var requiresConfirmedEmail = policies.RequiresConfirmedEmail && !user.EmailConfirmed;
var requiresUserApproval = policies.RequiresUserApproval && !user.Approved;
if (requiresConfirmedEmail)
if (policies.RequiresConfirmedEmail)
{
TempData[WellKnownTempData.SuccessMessage] += " Please confirm your email.";
}
if (requiresUserApproval)
if (policies.RequiresUserApproval)
{
TempData[WellKnownTempData.SuccessMessage] += " The new account requires approval by an admin before you can log in.";
}
if (requiresConfirmedEmail || requiresUserApproval)
if (policies.RequiresConfirmedEmail || policies.RequiresUserApproval)
{
return RedirectToAction(nameof(Login));
}
@ -650,11 +649,9 @@ 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 {Email} logged out", user!.Email);
_logger.LogInformation("User logged out");
return RedirectToAction(nameof(Login));
}
@ -673,31 +670,25 @@ namespace BTCPayServer.Controllers
}
var result = await _userManager.ConfirmEmailAsync(user, code);
if (result.Succeeded)
if (!await _userManager.HasPasswordAsync(user))
{
_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. Please set your password."
Message = "Your email has been confirmed but you still need to set your password."
});
return await RedirectToSetPassword(user);
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 View("Error");
@ -749,23 +740,17 @@ namespace BTCPayServer.Controllers
{
if (code == null)
{
throw new ApplicationException("A code must be supplied for this action.");
throw new ApplicationException("A code must be supplied for password reset.");
}
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;
}
return View(new SetPasswordViewModel
{
Code = code,
Email = email,
EmailSetInternally = !string.IsNullOrEmpty(email),
HasPassword = hasPassword
});
var model = new SetPasswordViewModel { Code = code, Email = email, EmailSetInternally = !string.IsNullOrEmpty(email) };
return View(model);
}
[HttpPost("/login/set-password")]
@ -777,9 +762,7 @@ 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
@ -792,76 +775,15 @@ namespace BTCPayServer.Controllers
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
Message = hasPassword ? "Password successfully set." : "Account successfully created."
Message = "Password successfully set."
});
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)

View File

@ -29,6 +29,7 @@ namespace BTCPayServer.Controllers
{
private readonly ThemeSettings _theme;
private readonly StoreRepository _storeRepository;
private readonly BTCPayNetworkProvider _networkProvider;
private IHttpClientFactory HttpClientFactory { get; }
private SignInManager<ApplicationUser> SignInManager { get; }
@ -40,12 +41,14 @@ 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;
@ -73,17 +76,17 @@ namespace BTCPayServer.Controllers
if (storeId != null)
{
// verify store exists and redirect to it
var store = await _storeRepository.FindStore(storeId);
var store = await _storeRepository.FindStore(storeId, userId);
if (store != null)
{
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId });
return RedirectToStore(userId, store);
}
}
var stores = await _storeRepository.GetStoresByUserId(userId!);
var stores = await _storeRepository.GetStoresByUserId(userId);
var activeStore = stores.FirstOrDefault(s => !s.Archived);
return activeStore != null
? RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId = activeStore.Id })
? RedirectToStore(userId, activeStore)
: RedirectToAction(nameof(UIUserStoresController.CreateStore), "UIUserStores");
}
@ -195,5 +198,14 @@ 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});
}
}
}

View File

@ -162,10 +162,10 @@ namespace BTCPayServer.Controllers
model.Overpaid = details.Overpaid;
model.StillDue = details.StillDue;
model.HasRates = details.HasRates;
if (additionalData.TryGetValue("receiptData", out object? receiptData))
if (additionalData.ContainsKey("receiptData"))
{
model.ReceiptData = (Dictionary<string, object>)receiptData;
model.ReceiptData = (Dictionary<string, object>)additionalData["receiptData"];
additionalData.Remove("receiptData");
}
@ -213,7 +213,7 @@ namespace BTCPayServer.Controllers
{
InvoiceId = i.Id,
OrderId = i.Metadata?.OrderId,
RedirectUrl = i.RedirectURL?.AbsoluteUri ?? i.Metadata?.OrderUrl,
OrderUrl = i.Metadata?.OrderUrl,
Status = i.Status.ToModernStatus(),
Currency = i.Currency,
Timestamp = i.InvoiceTime,
@ -226,42 +226,15 @@ namespace BTCPayServer.Controllers
{
return View(vm);
}
var metaData = PosDataParser.ParsePosData(i.Metadata?.ToJObject());
var additionalData = metaData
.Where(dict => !InvoiceAdditionalDataExclude.Contains(dict.Key))
.ToDictionary(dict => dict.Key, dict => dict.Value);
// Split receipt data into cart and additional data
if (additionalData.TryGetValue("receiptData", out object? combinedReceiptData))
{
var receiptData = new Dictionary<string, object>((Dictionary<string, object>)combinedReceiptData, StringComparer.OrdinalIgnoreCase);
string[] cartKeys = ["cart", "subtotal", "discount", "tip", "total"];
// extract cart data and lowercase keys to handle data uniformly in PosData partial
if (receiptData.Keys.Any(key => cartKeys.Contains(key.ToLowerInvariant())))
{
vm.CartData = new Dictionary<string, object>();
foreach (var key in cartKeys)
{
if (!receiptData.ContainsKey(key)) continue;
// add it to cart data and remove it from the general data
vm.CartData.Add(key.ToLowerInvariant(), receiptData[key]);
receiptData.Remove(key);
}
}
// assign the rest to additional data and remove empty values
if (receiptData.Any())
{
vm.AdditionalData = receiptData
.Where(x => !string.IsNullOrEmpty(x.Value.ToString()))
.ToDictionary(x => x.Key, x => x.Value);
}
}
JToken? receiptData = null;
i.Metadata?.AdditionalData?.TryGetValue("receiptData", out receiptData);
var payments = ViewPaymentRequestViewModel.PaymentRequestInvoicePayment.GetViewModels(i, _displayFormatter, _transactionLinkProviders);
vm.Amount = i.PaidAmount.Net;
vm.Payments = receipt.ShowPayments is false ? null : payments;
vm.AdditionalData = PosDataParser.ParsePosData(receiptData);
return View(print ? "InvoiceReceiptPrint" : "InvoiceReceipt", vm);
}
@ -368,11 +341,6 @@ namespace BTCPayServer.Controllers
accounting = paymentMethod.Calculate();
cryptoPaid = accounting.Paid;
dueAmount = accounting.TotalDue;
if (cryptoPaid is 0 && invoice is { Status: InvoiceStatusLegacy.Confirmed or InvoiceStatusLegacy.Complete, ExceptionStatus: InvoiceExceptionStatus.Marked })
{
cryptoPaid = accounting.TotalDue;
dueAmount = 0;
}
paidAmount = cryptoPaid.RoundToSignificant(appliedDivisibility);
}
@ -1105,7 +1073,7 @@ namespace BTCPayServer.Controllers
storeIds.Add(i);
}
model.Search = fs;
model.SearchText = fs.TextCombined;
model.SearchText = fs.TextSearch;
var apps = await _appService.GetAllApps(GetUserId(), false, storeId);
InvoiceQuery invoiceQuery = GetInvoiceQuery(fs, apps, timezoneOffset);
@ -1176,7 +1144,7 @@ namespace BTCPayServer.Controllers
[HttpGet("/stores/{storeId}/invoices/create")]
[HttpGet("invoices/create")]
[Authorize(Policy = Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice(InvoicesModel? model = null)
{
@ -1186,7 +1154,7 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
}
var store = await _StoreRepository.FindStore(model.StoreId);
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
if (store == null)
return NotFound();

View File

@ -90,14 +90,11 @@ namespace BTCPayServer
_pluginHookService = pluginHookService;
_invoiceActivator = invoiceActivator;
}
[EnableCors(CorsPolicies.All)]
[HttpGet("withdraw/pp/{pullPaymentId}")]
public Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, [FromQuery] string pr, CancellationToken cancellationToken)
{
return GetLNURLForPullPayment(cryptoCode, pullPaymentId, pr, pullPaymentId, cancellationToken);
}
[NonAction]
internal async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr, string k1, CancellationToken cancellationToken)
{
@ -299,11 +296,11 @@ namespace BTCPayServer
return NotFound();
}
var createInvoice = new CreateInvoiceRequest
var createInvoice = new CreateInvoiceRequest()
{
Amount = item?.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup ? null : item?.Price,
Amount = item?.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup? null: item?.Price,
Currency = currencyCode,
Checkout = new InvoiceDataBase.CheckoutOptions
Checkout = new InvoiceDataBase.CheckoutOptions()
{
RedirectURL = app.AppType switch
{
@ -315,7 +312,6 @@ namespace BTCPayServer
AdditionalSearchTerms = new[] { AppService.GetAppSearchTerm(app) }
};
var allowOverpay = item?.PriceType is not ViewPointOfSaleViewModel.ItemPriceType.Fixed;
var invoiceMetadata = new InvoiceMetadata { OrderId = AppService.GetRandomOrderId() };
if (item != null)
{
@ -330,7 +326,7 @@ namespace BTCPayServer
store.GetStoreBlob(),
createInvoice,
additionalTags: new List<string> { AppService.GetAppInternalTag(appId) },
allowOverpay: allowOverpay);
allowOverpay: false);
}
public class EditLightningAddressVM
@ -499,7 +495,7 @@ namespace BTCPayServer
});
}
public async Task<IActionResult> GetLNURLRequest(
private async Task<IActionResult> GetLNURLRequest(
string cryptoCode,
Data.StoreData store,
Data.StoreBlob blob,
@ -526,9 +522,7 @@ namespace BTCPayServer
return this.CreateAPIError(null, e.Message);
}
lnurlRequest = await CreateLNUrlRequestFromInvoice(cryptoCode, i, store, blob, lnurlRequest, lnUrlMetadata, allowOverpay);
return lnurlRequest is null
? BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Unable to create LNURL request." })
: Ok(lnurlRequest);
return lnurlRequest is null ? NotFound() : Ok(lnurlRequest);
}
private async Task<LNURLPayRequest> CreateLNUrlRequestFromInvoice(

View File

@ -50,7 +50,7 @@ namespace BTCPayServer.Controllers
$"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
}
_logger.LogInformation("User {Email} has disabled 2fa", user.Email);
_logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id);
return RedirectToAction(nameof(TwoFactorAuthentication));
}
@ -100,7 +100,7 @@ namespace BTCPayServer.Controllers
}
await _userManager.SetTwoFactorEnabledAsync(user, true);
_logger.LogInformation("User {Email} has enabled 2FA with an authenticator app", user.Email);
_logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id);
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 {Email} has reset their authentication app key", user.Email);
_logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id);
return RedirectToAction(nameof(EnableAuthenticator));
}

View File

@ -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) + ";"

View File

@ -108,7 +108,7 @@ namespace BTCPayServer.Controllers
}
[HttpGet("/stores/{storeId}/payment-requests/edit/{payReqId?}")]
[Authorize(Policy = Policies.CanModifyPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> EditPaymentRequest(string storeId, string payReqId)
{
var store = GetCurrentStore();
@ -277,10 +277,6 @@ 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 });

View File

@ -11,7 +11,6 @@ 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;
@ -128,27 +127,13 @@ namespace BTCPayServer.Controllers
if (_pullPaymentHostedService.SupportsLNURL(blob))
{
var url = Url.Action(nameof(UILNURLController.GetLNURLForPullPayment), "UILNURL", new { cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, pullPaymentId = vm.Id }, Request.Scheme, Request.Host.ToString());
var url = Url.Action("GetLNURLForPullPayment", "UILNURL", new { cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, pullPaymentId = vm.Id }, Request.Scheme, Request.Host.ToString());
vm.LnurlEndpoint = url != null ? new Uri(url) : null;
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)
@ -227,7 +212,6 @@ namespace BTCPayServer.Controllers
var supported = ppBlob.SupportedPaymentMethods;
PaymentMethodId paymentMethodId = null;
IClaimDestination destination = null;
string error = null;
if (string.IsNullOrEmpty(vm.SelectedPaymentMethod))
{
foreach (var pmId in supported)
@ -236,7 +220,6 @@ namespace BTCPayServer.Controllers
(IClaimDestination dst, string err) = handler == null
? (null, "No payment handler found for this payment method")
: await handler.ParseAndValidateClaimDestination(pmId, vm.Destination, ppBlob, cancellationToken);
error = err;
if (dst is not null && err is null)
{
paymentMethodId = pmId;
@ -249,15 +232,12 @@ namespace BTCPayServer.Controllers
{
paymentMethodId = supported.FirstOrDefault(id => vm.SelectedPaymentMethod == id.ToString());
var payoutHandler = paymentMethodId is null ? null : _payoutHandlers.FindPayoutHandler(paymentMethodId);
if (payoutHandler is not null)
{
(destination, error) = await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, vm.Destination, ppBlob, cancellationToken);
}
destination = payoutHandler is null ? null : (await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, vm.Destination, ppBlob, cancellationToken)).destination;
}
if (destination is null)
{
ModelState.AddModelError(nameof(vm.Destination), error ?? "Invalid destination or payment method");
ModelState.AddModelError(nameof(vm.Destination), "Invalid destination or payment method");
return await ViewPullPayment(pullPaymentId);
}

View File

@ -64,27 +64,7 @@ public partial class UIReportsController
decimal randomValue = ((decimal)rand.NextDouble() * range) + from;
return decimal.Round(randomValue, precision);
}
JObject GetFormattedAmount()
{
string? curr = null;
decimal value = 0m;
int offset = 0;
while (curr is null)
{
curr = row[fi - 1 - offset]?.ToString();
value = curr switch
{
"USD" or "EUR" or "CHF" => GenerateDecimal(30_000m, 60_000, 2),
"JPY" => GenerateDecimal(400_0000m, 1000_0000m, 0),
_ => 0.0m
};
if (value != 0.0m)
break;
curr = null;
offset++;
}
return DisplayFormatter.ToFormattedAmount(value, curr);
}
var fiatCurrency = rand.NextSingle() > 0.2 ? "USD" : TakeOne("JPY", "EUR", "CHF");
var cryptoCurrency = rand.NextSingle() > 0.2 ? "BTC" : TakeOne("LTC", "DOGE", "DASH");
@ -136,11 +116,14 @@ public partial class UIReportsController
return Encoders.Hex.EncodeData(GenerateBytes(32));
if (f.Name == "Rate")
{
return GetFormattedAmount();
}
if (f.Type == "amount")
{
return GetFormattedAmount();
var curr = row[fi - 1]?.ToString();
var value = curr switch
{
"USD" or "EUR" or "CHF" => GenerateDecimal(30_000m, 60_000, 2),
"JPY" => GenerateDecimal(400_0000m, 1000_0000m, 0),
_ => GenerateDecimal(30_000m, 60_000, 2)
};
return DisplayFormatter.ToFormattedAmount(value, curr);
}
return null;
}

View File

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
@ -15,25 +16,29 @@ 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;
switch (sortOrder)
model.DefaultRole = (await storeRepository.GetDefaultRole()).Role;
var roles = await storeRepository.GetStoreRoles(null);
if (sortOrder != null)
{
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;
switch (sortOrder)
{
case "desc":
ViewData["NextRoleSortOrder"] = "asc";
roles = roles.OrderByDescending(user => user.Role).ToArray();
break;
case "asc":
roles = roles.OrderBy(user => user.Role).ToArray();
ViewData["NextRoleSortOrder"] = "desc";
break;
}
}
model.Roles = roles.Skip(model.Skip).Take(model.Count).ToList();
@ -42,26 +47,32 @@ namespace BTCPayServer.Controllers
}
[HttpGet("server/roles/{role}")]
public async Task<IActionResult> CreateOrEditRole(string role)
public async Task<IActionResult> CreateOrEditRole(
[FromServices] StoreRepository storeRepository,
string role)
{
if (role == "create")
{
ModelState.Remove(nameof(role));
return View(new UpdateRoleViewModel());
}
var roleData = await _StoreRepository.GetStoreRole(new StoreRoleId(role));
if (roleData == null)
return NotFound();
return View(new UpdateRoleViewModel
else
{
Policies = roleData.Permissions,
Role = roleData.Role
});
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(role));
if (roleData == null)
return NotFound();
return View(new UpdateRoleViewModel()
{
Policies = roleData.Permissions,
Role = roleData.Role
});
}
}
[HttpPost("server/roles/{role}")]
public async Task<IActionResult> CreateOrEditRole([FromRoute] string role, UpdateRoleViewModel viewModel)
public async Task<IActionResult> CreateOrEditRole(
[FromServices] StoreRepository storeRepository,
[FromRoute] string role, UpdateRoleViewModel viewModel)
{
string successMessage = null;
if (role == "create")
@ -72,7 +83,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();
}
@ -82,7 +93,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()
@ -105,9 +116,11 @@ namespace BTCPayServer.Controllers
[HttpGet("server/roles/{role}/delete")]
public async Task<IActionResult> DeleteRole(string role)
public async Task<IActionResult> DeleteRole(
[FromServices] StoreRepository storeRepository,
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();
@ -121,10 +134,12 @@ namespace BTCPayServer.Controllers
}
[HttpPost("server/roles/{role}/delete")]
public async Task<IActionResult> DeleteRolePost(string role)
public async Task<IActionResult> DeleteRolePost(
[FromServices] StoreRepository storeRepository,
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)
@ -132,7 +147,7 @@ namespace BTCPayServer.Controllers
return BadRequest();
}
var errorMessage = await _StoreRepository.RemoveStoreRole(roleId);
var errorMessage = await storeRepository.RemoveStoreRole(roleId);
if (errorMessage is null)
{
@ -147,16 +162,19 @@ namespace BTCPayServer.Controllers
}
[HttpGet("server/roles/{role}/default")]
public async Task<IActionResult> SetDefaultRole(string role)
public async Task<IActionResult> SetDefaultRole(
[FromServices] StoreRepository storeRepository,
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";
}

View File

@ -9,7 +9,6 @@ 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;
@ -103,7 +102,7 @@ namespace BTCPayServer.Controllers
bool? adminStatusChanged = null;
bool? approvalStatusChanged = null;
if (user.RequiresApproval && viewModel.Approved.HasValue && user.Approved != viewModel.Approved.Value)
if (user.RequiresApproval && viewModel.Approved.HasValue)
{
approvalStatusChanged = await _userService.SetUserApproval(user.Id, viewModel.Approved.Value, Request.GetAbsoluteRootUri());
}
@ -150,6 +149,7 @@ namespace BTCPayServer.Controllers
[HttpGet("server/users/new")]
public IActionResult CreateUser()
{
ViewData["AllowRequestApproval"] = _policiesSettings.RequiresUserApproval;
ViewData["AllowRequestEmailConfirmation"] = _policiesSettings.RequiresConfirmedEmail;
return View();
}
@ -157,11 +157,13 @@ 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,
@ -169,13 +171,18 @@ namespace BTCPayServer.Controllers
EmailConfirmed = model.EmailConfirmed,
RequiresEmailConfirmation = _policiesSettings.RequiresConfirmedEmail,
RequiresApproval = _policiesSettings.RequiresUserApproval,
Approved = true, // auto-approve users created by an admin
Approved = model.Approved,
Created = DateTimeOffset.UtcNow
};
var result = string.IsNullOrEmpty(model.Password)
? await _UserManager.CreateAsync(user)
: await _UserManager.CreateAsync(user, model.Password);
if (!string.IsNullOrEmpty(model.Password))
{
result = await _UserManager.CreateAsync(user, model.Password);
}
else
{
result = await _UserManager.CreateAsync(user);
}
if (result.Succeeded)
{
@ -183,30 +190,37 @@ 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,
InvitedByUser = currentUser,
Admin = model.IsAdmin,
Admin = model.IsAdmin is true,
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";
TempData.SetStatusMessageModel(new StatusMessageModel
if (user.RequiresEmailConfirmation && !user.EmailConfirmed)
{
Severity = StatusMessageModel.StatusSeverity.Success,
AllowDismiss = false,
Html = $"Account successfully created. {info} share this link with them: <a class='alert-link' href='{callbackUrl}'>{callbackUrl}</a>"
});
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>"
});
}
return RedirectToAction(nameof(ListUsers));
}
@ -363,5 +377,8 @@ namespace BTCPayServer.Controllers
[Display(Name = "Email confirmed?")]
public bool EmailConfirmed { get; set; }
[Display(Name = "User approved?")]
public bool Approved { get; set; }
}
}

View File

@ -16,7 +16,9 @@ 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;
@ -24,8 +26,10 @@ 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;
@ -1049,22 +1053,11 @@ namespace BTCPayServer.Controllers
vm.LogoFileId = theme.LogoFileId;
vm.CustomThemeFileId = theme.CustomThemeFileId;
if (server.ServerName != vm.ServerName)
if (server.ServerName != vm.ServerName || server.ContactUrl != vm.ContactUrl)
{
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);
}
@ -1187,7 +1180,7 @@ namespace BTCPayServer.Controllers
return View(vm);
}
[HttpGet("server/emails")]
[Route("server/emails")]
public async Task<IActionResult> Emails()
{
var email = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
@ -1198,7 +1191,8 @@ namespace BTCPayServer.Controllers
return View(vm);
}
[HttpPost("server/emails")]
[Route("server/emails")]
[HttpPost]
public async Task<IActionResult> Emails(ServerEmailsViewModel model, string command)
{
if (command == "Test")
@ -1215,10 +1209,8 @@ 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), $"{serverName}: Email test", "You received it, the BTCPay Server SMTP settings work.", false))
using (var message = model.Settings.CreateMailMessage(MailboxAddress.Parse(model.TestEmail), "BTCPay test", "BTCPay test", false))
{
await client.SendAsync(message);
await client.DisconnectAsync(true);
@ -1254,7 +1246,7 @@ namespace BTCPayServer.Controllers
return View(model);
}
var oldSettings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
if (new ServerEmailsViewModel(oldSettings).PasswordSet)
if (new EmailsViewModel(oldSettings).PasswordSet)
{
model.Settings.Password = oldSettings.Password;
}

View File

@ -10,9 +10,10 @@ 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.PayoutProcessors;
using BTCPayServer.Rating;
using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
@ -22,6 +23,7 @@ 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
@ -38,8 +40,6 @@ 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,8 +55,6 @@ namespace BTCPayServer.Controllers
DisplayFormatter displayFormatter,
PullPaymentHostedService pullPaymentHostedService,
ApplicationDbContextFactory dbContextFactory,
PayoutProcessorService payoutProcessorService,
IEnumerable<IPayoutProcessorFactory> payoutProcessorFactories,
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
IAuthorizationService authorizationService)
{
@ -68,10 +66,8 @@ 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)
@ -87,7 +83,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.Index), "UIStores", new { storeId });
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId });
}
return View(new NewPullPaymentModel
@ -165,7 +161,6 @@ 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,
@ -204,7 +199,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.Index), "UIStores", new { storeId });
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId });
}
var vm = this.ParseListQuery(new PullPaymentsModel
@ -291,7 +286,6 @@ 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);
@ -375,7 +369,7 @@ namespace BTCPayServer.Controllers
break;
}
if (command == "approve-pay" && !vm.HasPayoutProcessor)
if (command == "approve-pay")
{
goto case "pay";
}
@ -488,21 +482,19 @@ namespace BTCPayServer.Controllers
Message = "You must enable at least one payment method before creating a payout.",
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId });
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId });
}
paymentMethodId ??= paymentMethods.First().ToString();
var vm = this.ParseListQuery(new PayoutsModel
{
PaymentMethods = paymentMethods,
PaymentMethodId = paymentMethodId,
PaymentMethodId = paymentMethodId ?? paymentMethods.First().ToString(),
PullPaymentId = pullPaymentId,
PayoutState = payoutState,
Skip = skip,
Count = count,
Payouts = new List<PayoutsModel.PayoutModel>(),
HasPayoutProcessor = await HasPayoutProcessor(storeId, paymentMethodId)
Count = count
});
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));
@ -584,13 +576,5 @@ 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();
}
}
}

View File

@ -1,15 +1,12 @@
#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
@ -17,13 +14,9 @@ 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,
@ -45,17 +38,16 @@ namespace BTCPayServer.Controllers
};
// Widget data
if (vm is { WalletEnabled: false, LightningEnabled: false })
if (!vm.WalletEnabled && !vm.LightningEnabled)
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.GetAppData(userId, app.Id);
var appData = await _appService.GetAppDataIfOwner(userId, app.Id);
vm.Apps.Add(appData);
}
@ -63,7 +55,6 @@ 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();
@ -75,7 +66,6 @@ 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();
@ -87,7 +77,6 @@ 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();
@ -99,7 +88,6 @@ 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();

View File

@ -7,11 +7,9 @@ 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;
@ -45,7 +43,6 @@ 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>();
@ -121,7 +118,7 @@ namespace BTCPayServer.Controllers
.Where(o => o != null)
.ToArray();
emailSender.SendEmail(recipients.ToArray(), null, null, $"[TEST] {rule.Subject}", rule.Body);
emailSender.SendEmail(recipients.ToArray(), null, null, $"({store.StoreName} test) {rule.Subject}", rule.Body);
message += "Test email sent — please verify you received it.";
}
else
@ -188,41 +185,33 @@ namespace BTCPayServer.Controllers
}
[HttpPost("{storeId}/email-settings")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> StoreEmailSettings(string storeId, EmailsViewModel model, string command, [FromForm] bool useCustomSMTP = false)
public async Task<IActionResult> StoreEmailSettings(string storeId, EmailsViewModel model, string command)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
model.FallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender
var emailSender = await _emailSenderFactory.GetEmailSender(store.Id) as StoreEmailSender;
var fallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender
? await storeSender.FallbackSender.GetEmailSettings()
: null;
if (model.FallbackSettings is null) useCustomSMTP = true;
ViewBag.UseCustomSMTP = useCustomSMTP;
if (useCustomSMTP)
{
model.Settings.Validate("Settings.", ModelState);
}
model.FallbackSettings = fallbackSettings;
if (command == "Test")
{
try
{
if (useCustomSMTP)
if (model.PasswordSet)
{
if (model.PasswordSet)
{
model.Settings.Password = store.GetStoreBlob().EmailSettings.Password;
}
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);
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);
using var client = await model.Settings.CreateSmtpClient();
var message = model.Settings.CreateMailMessage(MailboxAddress.Parse(model.TestEmail), "BTCPay test", "BTCPay test", false);
await client.SendAsync(message);
await client.DisconnectAsync(true);
TempData[WellKnownTempData.SuccessMessage] = $"Email sent to {model.TestEmail}. Please verify you received it.";
@ -240,17 +229,17 @@ namespace BTCPayServer.Controllers
store.SetStoreBlob(storeBlob);
await _Repo.UpdateStore(store);
TempData[WellKnownTempData.SuccessMessage] = "Email server password reset";
return RedirectToAction(nameof(StoreEmailSettings), new { storeId });
}
if (useCustomSMTP)
else // if (command == "Save")
{
if (model.Settings.From is not null && !MailboxAddressValidator.IsMailboxAddress(model.Settings.From))
{
ModelState.AddModelError("Settings.From", "Invalid email");
}
if (!ModelState.IsValid)
return View(model);
}
var storeBlob = store.GetStoreBlob();
if (storeBlob.EmailSettings != null && new EmailsViewModel(storeBlob.EmailSettings, model.FallbackSettings).PasswordSet)
if (storeBlob.EmailSettings != null && new EmailsViewModel(storeBlob.EmailSettings, fallbackSettings).PasswordSet)
{
model.Settings.Password = storeBlob.EmailSettings.Password;
}
@ -258,8 +247,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)

View File

@ -4,12 +4,10 @@ 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;
@ -48,7 +46,6 @@ namespace BTCPayServer.Controllers
}
[HttpGet("{storeId}/webhooks/new")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public IActionResult NewWebhook()
{
return View(nameof(ModifyWebhook), new EditWebhookViewModel
@ -61,7 +58,6 @@ 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);
@ -72,7 +68,6 @@ 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);
@ -85,7 +80,6 @@ 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)
@ -97,7 +91,6 @@ 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);
@ -114,7 +107,6 @@ 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);
@ -129,7 +121,6 @@ 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);
@ -140,7 +131,6 @@ 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);
@ -158,7 +148,6 @@ 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);
@ -179,7 +168,6 @@ 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);

View File

@ -5,7 +5,7 @@ using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Components.StoreLightningBalance;
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 Microsoft.AspNetCore.Authorization;
using BTCPayServer.Services;
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,7 +85,6 @@ 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();
@ -102,7 +101,6 @@ 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;
@ -219,7 +217,6 @@ 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();
@ -260,7 +257,6 @@ 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();
@ -314,7 +310,6 @@ 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();

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
@ -7,12 +8,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 Microsoft.AspNetCore.Authorization;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
@ -26,7 +27,6 @@ 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,7 +42,6 @@ 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);
@ -72,7 +71,6 @@ 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);
@ -199,7 +197,6 @@ 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);
@ -238,11 +235,8 @@ 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);
@ -362,7 +356,6 @@ 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);
@ -378,7 +371,6 @@ 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);
@ -448,7 +440,6 @@ 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 _);
@ -558,7 +549,6 @@ 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 _);
@ -622,7 +612,6 @@ 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);
@ -669,7 +658,6 @@ 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);
@ -689,7 +677,6 @@ 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 _);
@ -708,7 +695,6 @@ 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);
@ -728,7 +714,6 @@ 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);

View File

@ -1,19 +1,22 @@
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
{
[HttpGet("{storeId}/roles")]
[Route("{storeId}/roles")]
public async Task<IActionResult> ListRoles(
string storeId,
[FromServices] StoreRepository storeRepository,
@ -21,21 +24,24 @@ 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;
switch (sortOrder)
model.DefaultRole = (await storeRepository.GetDefaultRole()).Role;
var roles = await storeRepository.GetStoreRoles(storeId, false, false);
if (sortOrder != null)
{
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;
switch (sortOrder)
{
case "desc":
ViewData["NextRoleSortOrder"] = "asc";
roles = roles.OrderByDescending(user => user.Role).ToArray();
break;
case "asc":
roles = roles.OrderBy(user => user.Role).ToArray();
ViewData["NextRoleSortOrder"] = "desc";
break;
}
}
model.Roles = roles.Skip(model.Skip).Take(model.Count).ToList();
@ -44,7 +50,6 @@ namespace BTCPayServer.Controllers
}
[HttpGet("{storeId}/roles/{role}")]
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> CreateOrEditRole(
string storeId,
[FromServices] StoreRepository storeRepository,
@ -55,19 +60,19 @@ namespace BTCPayServer.Controllers
ModelState.Remove(nameof(role));
return View(new UpdateRoleViewModel());
}
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(storeId, role));
if (roleData == null)
return NotFound();
return View(new UpdateRoleViewModel
else
{
Policies = roleData.Permissions,
Role = roleData.Role
});
}
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(storeId, role));
if (roleData == null)
return NotFound();
return View(new UpdateRoleViewModel()
{
Policies = roleData.Permissions,
Role = roleData.Role
});
}
}
[HttpPost("{storeId}/roles/{role}")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> CreateOrEditRole(
string storeId,
[FromServices] StoreRepository storeRepository,
@ -114,9 +119,10 @@ 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,
@ -136,7 +142,6 @@ 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,

View File

@ -1,172 +0,0 @@
#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();
}
}
}

View File

@ -8,12 +8,11 @@ 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;
using BTCPayServer.HostedServices.Webhooks;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
@ -40,9 +39,10 @@ using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers
{
[Route("stores")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[AutoValidateAntiforgeryToken]
public partial class UIStoresController : Controller
{
@ -62,6 +62,7 @@ namespace BTCPayServer.Controllers
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
PoliciesSettings policiesSettings,
IAuthorizationService authorizationService,
EventAggregator eventAggregator,
AppService appService,
IFileService fileService,
WebhookSender webhookNotificationManager,
@ -71,9 +72,7 @@ namespace BTCPayServer.Controllers
IHtmlHelper html,
LightningClientFactoryService lightningClientFactoryService,
EmailSenderFactory emailSenderFactory,
WalletFileParsers onChainWalletParsers,
SettingsRepository settingsRepository,
EventAggregator eventAggregator)
WalletFileParsers onChainWalletParsers)
{
_RateFactory = rateFactory;
_Repo = repo;
@ -100,8 +99,6 @@ namespace BTCPayServer.Controllers
_lightningClientFactoryService = lightningClientFactoryService;
_emailSenderFactory = emailSenderFactory;
_onChainWalletParsers = onChainWalletParsers;
_settingsRepository = settingsRepository;
_eventAggregator = eventAggregator;
Html = html;
}
@ -115,7 +112,6 @@ 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;
@ -128,7 +124,6 @@ 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; }
@ -150,22 +145,97 @@ namespace BTCPayServer.Controllers
if (string.IsNullOrEmpty(userId))
return Forbid();
var store = await _Repo.FindStore(storeId);
var store = await _Repo.FindStore(storeId, userId);
if (store is null)
return NotFound();
if ((await _authorizationService.AuthorizeAsync(User, Policies.CanModifyStoreSettings)).Succeeded)
{
return Forbid();
}
HttpContext.SetStoreData(store);
if (store.GetPermissionSet(userId).Contains(Policies.CanModifyStoreSettings, storeId))
{
return RedirectToAction("Dashboard", new { storeId });
}
if ((await _authorizationService.AuthorizeAsync(User, Policies.CanViewInvoices)).Succeeded)
if (store.GetPermissionSet(userId).Contains(Policies.CanViewInvoices, storeId))
{
return RedirectToAction("ListInvoices", "UIInvoice", new { storeId });
}
return Forbid();
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 });
}
public StoreData? CurrentStore => HttpContext.GetStoreData();
[HttpGet("{storeId}/rates")]
public IActionResult Rates()
@ -185,7 +255,6 @@ 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")
@ -304,7 +373,6 @@ namespace BTCPayServer.Controllers
}
[HttpGet("{storeId}/rates/confirm")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public IActionResult ShowRateRules(bool scripting)
{
return View("Confirm", new ConfirmModel
@ -319,7 +387,6 @@ 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();
@ -420,7 +487,6 @@ 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;
@ -548,7 +614,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.IsValidEmail() ? $"mailto:{model.SupportUrl}" : model.SupportUrl;
blob.StoreSupportUrl = string.IsNullOrWhiteSpace(model.SupportUrl) ? null : model.SupportUrl;
blob.DisplayExpirationTimer = TimeSpan.FromMinutes(model.DisplayExpirationTimer);
blob.AutoDetectLanguage = model.AutoDetectLanguage;
blob.DefaultLang = model.DefaultLang;
@ -657,7 +723,6 @@ namespace BTCPayServer.Controllers
}
[HttpPost("{storeId}/settings")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> GeneralSettings(
GeneralSettingsViewModel model,
[FromForm] bool RemoveLogoFile = false,
@ -798,7 +863,7 @@ namespace BTCPayServer.Controllers
}
[HttpPost("{storeId}/archive")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettings)]
public async Task<IActionResult> ToggleArchive(string storeId)
{
CurrentStore.Archived = !CurrentStore.Archived;
@ -815,14 +880,12 @@ 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);
@ -861,7 +924,6 @@ namespace BTCPayServer.Controllers
}
[HttpGet("{storeId}/tokens")]
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> ListTokens()
{
var model = new TokensViewModel();
@ -883,7 +945,6 @@ 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);
@ -893,7 +954,6 @@ 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);
@ -907,7 +967,6 @@ 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);
@ -917,7 +976,6 @@ namespace BTCPayServer.Controllers
}
[HttpGet("{storeId}/tokens/create")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public IActionResult CreateToken(string storeId)
{
var model = new CreateTokenViewModel();
@ -929,7 +987,6 @@ 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)
@ -1008,7 +1065,6 @@ 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();
@ -1073,7 +1129,6 @@ 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)

View File

@ -6,6 +6,7 @@ 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;

View File

@ -151,12 +151,13 @@ namespace BTCPayServer.Controllers
WalletId walletId, WalletPSBTViewModel vm, string command = null)
{
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
var psbt = await vm.GetPSBT(network.NBitcoinNetwork, ModelState);
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
vm.BackUrl ??= HttpContext.Request.GetTypedHeaders().Referer?.AbsolutePath;
if (psbt is null || vm.InvalidPSBT)
{
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
return View("WalletSigningOptions", new WalletSigningOptionsModel
{
SigningContext = vm.SigningContext,
@ -240,9 +241,10 @@ namespace BTCPayServer.Controllers
vm.NBXSeedAvailable = await CanUseHotWallet() && derivationSchemeSettings.IsHotWallet;
vm.BackUrl ??= HttpContext.Request.GetTypedHeaders().Referer?.AbsolutePath;
var psbt = await vm.GetPSBT(network.NBitcoinNetwork, ModelState);
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
if (vm.InvalidPSBT)
{
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
return View(vm);
}
if (psbt is null)
@ -475,7 +477,7 @@ namespace BTCPayServer.Controllers
WalletId walletId, WalletPSBTViewModel vm, string command, CancellationToken cancellationToken = default)
{
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
PSBT psbt = await vm.GetPSBT(network.NBitcoinNetwork, ModelState);
PSBT psbt = await vm.GetPSBT(network.NBitcoinNetwork);
if (vm.InvalidPSBT || psbt is null)
{
if (vm.InvalidPSBT)
@ -635,14 +637,16 @@ namespace BTCPayServer.Controllers
WalletId walletId, WalletPSBTCombineViewModel vm)
{
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
var psbt = await vm.GetPSBT(network.NBitcoinNetwork, ModelState);
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
if (psbt == null)
{
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
return View(vm);
}
var sourcePSBT = vm.GetSourcePSBT(network.NBitcoinNetwork, ModelState);
if (sourcePSBT is null)
var sourcePSBT = vm.GetSourcePSBT(network.NBitcoinNetwork);
if (sourcePSBT == null)
{
ModelState.AddModelError(nameof(vm.OtherPSBT), "Invalid PSBT");
return View(vm);
}
sourcePSBT = sourcePSBT.Combine(psbt);

View File

@ -439,7 +439,7 @@ namespace BTCPayServer.Controllers
{
if (walletId?.StoreId == null)
return NotFound();
var store = await Repository.FindStore(walletId.StoreId);
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
var paymentMethod = GetDerivationSchemeSettings(walletId);
if (paymentMethod == null || store is null)
return NotFound();
@ -459,12 +459,11 @@ 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, messagePresent);
await LoadFromBIP21(walletId, model, link, network);
}
}
}
@ -540,6 +539,7 @@ 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);
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
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, TempData.HasStatusMessage());
await LoadFromBIP21(walletId, vm, bip21, network);
}
decimal transactionAmountSum = 0;
@ -738,7 +738,7 @@ namespace BTCPayServer.Controllers
foreach (var transactionOutput in vm.Outputs.Where(output => output.Labels?.Any() is true))
{
var labels = transactionOutput.Labels.Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
var walletObjectAddress = new WalletObjectId(walletId, WalletObjectData.Types.Address, transactionOutput.DestinationAddress);
var walletObjectAddress = new WalletObjectId(walletId, WalletObjectData.Types.Address, transactionOutput.DestinationAddress.ToLowerInvariant());
var obj = await WalletRepository.GetWalletObject(walletObjectAddress);
if (obj is null)
{
@ -870,7 +870,7 @@ namespace BTCPayServer.Controllers
private async Task LoadFromBIP21(WalletId walletId, WalletSendModel vm, string bip21,
BTCPayNetwork network, bool statusMessagePresent)
BTCPayNetwork network)
{
BitcoinAddress? address = null;
vm.Outputs ??= new();
@ -892,18 +892,14 @@ namespace BTCPayServer.Controllers
}
vm.Outputs.Add(output);
address = uriBuilder.Address;
// only set SetStatusMessageModel if there is not message already or there is label / message in uri builder
if (!statusMessagePresent)
if (!string.IsNullOrEmpty(uriBuilder.Label) || !string.IsNullOrEmpty(uriBuilder.Message))
{
if (!string.IsNullOrEmpty(uriBuilder.Label) || !string.IsNullOrEmpty(uriBuilder.Message))
TempData.SetStatusMessageModel(new StatusMessageModel
{
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>")}"
});
}
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 _))

View File

@ -134,7 +134,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
return raw.ToObject<ManualPayoutProof>();
}
public static void ParseProofType(string proof, out JObject obj, out string type)
public static void ParseProofType(byte[] 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(proof);
obj = JObject.Parse(Encoding.UTF8.GetString(proof));
TryParseProofType(obj, out type);
}

View File

@ -11,7 +11,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
Bolt11 = bolt11 ?? throw new ArgumentNullException(nameof(bolt11));
PaymentRequest = paymentRequest;
PaymentHash = paymentRequest.Hash;
Amount = paymentRequest.MinimumAmount.MilliSatoshi == LightMoney.Zero ? null: paymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
Amount = paymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
}
public override string ToString()

View File

@ -73,11 +73,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
{
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using var t = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken);
var rawInfo = await LNURL.LNURL.FetchInformation(lnurl, CreateClient(lnurl), t.Token);
if(rawInfo is null)
return (null, "The LNURL / Lightning Address provided was not online.");
if(rawInfo is not LNURLPayRequest info)
return (null, "The LNURL was not a valid LNURL Pay request.");
var info = (LNURLPayRequest)(await LNURL.LNURL.FetchInformation(lnurl, CreateClient(lnurl), t.Token));
lnurlTag = info.Tag;
}
@ -147,27 +143,9 @@ namespace BTCPayServer.Data.Payouts.LightningLike
return Task.CompletedTask;
}
public async Task<decimal> GetMinimumPayoutAmount(PaymentMethodId paymentMethod, IClaimDestination claimDestination)
public Task<decimal> GetMinimumPayoutAmount(PaymentMethodId paymentMethodId, IClaimDestination claimDestination)
{
if(claimDestination is LNURLPayClaimDestinaton lnurlPayClaimDestinaton)
{
try
{
var lnurl = lnurlPayClaimDestinaton.LNURL.IsValidEmail()
? LNURL.LNURL.ExtractUriFromInternetIdentifier(lnurlPayClaimDestinaton.LNURL)
: LNURL.LNURL.Parse(lnurlPayClaimDestinaton.LNURL, out var lnurlTag);
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var rawInfo = await LNURL.LNURL.FetchInformation(lnurl, CreateClient(lnurl), timeout.Token);
if (rawInfo is LNURLPayRequest info)
return info.MinSendable.ToDecimal(LightMoneyUnit.BTC);
}
catch
{
// ignored
}
}
return Money.Satoshis(1).ToDecimal(MoneyUnit.BTC);
return Task.FromResult(Money.Satoshis(1).ToDecimal(MoneyUnit.BTC));
}
public Dictionary<PayoutState, List<(string Action, string Text)>> GetPayoutSpecificActions()

View File

@ -244,15 +244,13 @@ namespace BTCPayServer.Data.Payouts.LightningLike
var lm = new LightMoney(blob.CryptoAmount.Value, LightMoneyUnit.BTC);
if (lm > lnurlInfo.MaxSendable || lm < lnurlInfo.MinSendable)
{
payoutData.State = PayoutState.Cancelled;
return (null, new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Destination = blob.Destination,
Message =
$"The LNURL provided would not generate an invoice of {lm.ToDecimal(LightMoneyUnit.Satoshi)} sats"
$"The LNURL provided would not generate an invoice of {lm.MilliSatoshi}msats"
});
}

View File

@ -44,18 +44,18 @@ namespace BTCPayServer.Data
public static PayoutBlob GetBlob(this PayoutData data, BTCPayNetworkJsonSerializerSettings serializers)
{
var result = JsonConvert.DeserializeObject<PayoutBlob>(data.Blob, serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode));
var result = JsonConvert.DeserializeObject<PayoutBlob>(Encoding.UTF8.GetString(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 = JsonConvert.SerializeObject(blob, serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode)).ToString();
data.Blob = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob, serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode)));
}
public static JObject GetProofBlobJson(this PayoutData data)
{
return data?.Proof is null ? null : JObject.Parse(data.Proof);
return data?.Proof is null ? null : JObject.Parse(Encoding.UTF8.GetString(data.Proof));
}
public static void SetProofBlob(this PayoutData data, IPayoutProof blob, JsonSerializerSettings settings)
{
@ -76,10 +76,11 @@ 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 (!JToken.DeepEquals(blob, data.Proof is null ? null : JObject.Parse(data.Proof)))
if (data.Proof is null || bytes.Length != data.Proof.Length || !bytes.SequenceEqual(data.Proof))
{
data.Proof = blob.ToString(Formatting.None);
data.Proof = bytes;
}
}

View File

@ -10,13 +10,13 @@ namespace BTCPayServer.Data
public static PullPaymentBlob GetBlob(this PullPaymentData data)
{
var result = JsonConvert.DeserializeObject<PullPaymentBlob>(data.Blob);
var result = JsonConvert.DeserializeObject<PullPaymentBlob>(Encoding.UTF8.GetString(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 = JsonConvert.SerializeObject(blob).ToString();
data.Blob = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob));
}
public static bool IsSupported(this PullPaymentData data, Payments.PaymentMethodId paymentId)

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using NBitcoin;
using NBitcoin.Scripting;
using NBXplorer.DerivationStrategy;
@ -101,7 +100,6 @@ namespace BTCPayServer
{
ArgumentNullException.ThrowIfNull(str);
str = str.Trim();
str = Regex.Replace(str, @"\s+", "");
HashSet<string> hintedLabels = new HashSet<string>();
if (!Network.Consensus.SupportSegwit)
{

View File

@ -1,10 +1,12 @@
using System;
using BTCPayServer.Data;
namespace BTCPayServer.Events;
public class UserApprovedEvent
namespace BTCPayServer.Events
{
public ApplicationUser User { get; set; }
public Uri RequestUri { get; set; }
public class UserApprovedEvent
{
public ApplicationUser User { get; set; }
public bool Approved { get; set; }
public Uri RequestUri { get; set; }
}
}

View File

@ -1,10 +0,0 @@
using System;
using BTCPayServer.Data;
namespace BTCPayServer.Events;
public class UserConfirmedEmailEvent
{
public ApplicationUser User { get; set; }
public Uri RequestUri { get; set; }
}

View File

@ -1,10 +0,0 @@
using System;
using BTCPayServer.Data;
namespace BTCPayServer.Events;
public class UserInviteAcceptedEvent
{
public ApplicationUser User { get; set; }
public Uri RequestUri { get; set; }
}

View File

@ -2,20 +2,14 @@ using System;
using System.Threading.Tasks;
using BTCPayServer.Data;
namespace BTCPayServer.Events;
public class UserRegisteredEvent
namespace BTCPayServer.Events
{
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 class UserRegisteredEvent
{
public ApplicationUser User { get; set; }
public bool Admin { get; set; }
public Uri RequestUri { get; set; }
public enum UserRegisteredEventKind
{
Registration,
Invite
public TaskCompletionSource<Uri> CallbackUrlGenerated;
}
}

View File

@ -12,49 +12,28 @@ namespace BTCPayServer.Services
private static string CallToAction(string actionName, string actionLink)
{
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>";
string button = $"{BUTTON_HTML}".Replace("{button_description}", actionName, System.StringComparison.InvariantCulture);
button = button.Replace("{button_link}", actionLink, System.StringComparison.InvariantCulture);
return button;
}
public static void SendEmailConfirmation(this IEmailSender emailSender, MailboxAddress address, string link)
{
emailSender.SendEmail(address, "Confirm your email", CreateEmailBody(
$"Please confirm your account.<br/><br/>{CallToAction("Confirm Email", link)}"));
emailSender.SendEmail(address, "Confirm your email",
$"Please confirm your account by clicking this link: <a href='{HtmlEncoder.Default.Encode(link)}'>link</a>");
}
public static void SendApprovalConfirmation(this IEmailSender emailSender, MailboxAddress address, string link)
{
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>."));
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>");
}
public static void SendResetPassword(this IEmailSender emailSender, MailboxAddress address, string link)
public static void SendSetPasswordConfirmation(this IEmailSender emailSender, MailboxAddress address, string link, bool newPassword)
{
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>"));
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>");
}
}
}

View File

@ -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)

View File

@ -23,25 +23,6 @@ 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",
@ -52,7 +33,8 @@ namespace Microsoft.AspNetCore.Mvc
{
return urlHelper.GetUriByAction(nameof(UIAccountController.Login), "UIAccount", null , scheme, host, pathbase);
}
public static string ResetPasswordLink(this LinkGenerator urlHelper, string userId, string code, string scheme, HostString host, string pathbase)
public static string ResetPasswordCallbackLink(this LinkGenerator urlHelper, string userId, string code, string scheme, HostString host, string pathbase)
{
return urlHelper.GetUriByAction(
action: nameof(UIAccountController.SetPassword),

View File

@ -37,11 +37,11 @@ namespace BTCPayServer.Filters
}
// If we have an appId, we can redirect to the canonical domain
if ((string)context.RouteContext.RouteData.Values["appId"] is { } appId)
if ((string)context.RouteContext.RouteData.Values["appId"] is { } appId && !req.IsOnion())
{
var redirectDomain = mapping.FirstOrDefault(item => item.AppId == appId)?.Domain;
// App is accessed via path, redirect to canonical domain
if (!string.IsNullOrEmpty(redirectDomain) && req.Method != "POST" && !req.HasFormContentType && !req.IsOnion())
if (!string.IsNullOrEmpty(redirectDomain) && req.Method != "POST" && !req.HasFormContentType)
{
var uri = new UriBuilder(req.Scheme, redirectDomain);
if (req.Host.Port.HasValue)

View File

@ -22,7 +22,7 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Forms;
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public class UIFormsController : Controller
{
private readonly FormDataService _formDataService;
@ -48,7 +48,6 @@ 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() };
@ -56,7 +55,6 @@ 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);
@ -68,7 +66,6 @@ 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)
@ -125,7 +122,6 @@ 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);

View File

@ -135,7 +135,7 @@ namespace BTCPayServer.HostedServices
(await _EmailSenderFactory.GetEmailSender(invoice.StoreId)).SendEmail(
notificationEmail,
$"Invoice Notification - ${invoice.StoreId}",
$"{storeName} Invoice Notification - ${invoice.StoreId}",
emailBody);
}

View File

@ -7,13 +7,11 @@ using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Logging;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs;
using BTCPayServer.Services.Rates;
@ -49,7 +47,7 @@ namespace BTCPayServer.HostedServices
public class PullPaymentHostedService : BaseAsyncService
{
private readonly string[] _lnurlSupportedCurrencies = { "BTC", "SATS" };
public class CancelRequest
{
public CancelRequest(string pullPaymentId)
@ -109,23 +107,7 @@ namespace BTCPayServer.HostedServices
}
}
}
public Task<string> CreatePullPayment(string storeId, CreatePullPaymentRequest request)
{
return CreatePullPayment(new CreatePullPayment()
{
StartsAt = request.StartsAt,
ExpiresAt = request.ExpiresAt,
Period = request.Period,
BOLT11Expiration = request.BOLT11Expiration,
Name = request.Name,
Description = request.Description,
Amount = request.Amount,
Currency = request.Currency,
StoreId = storeId,
PaymentMethodIds = request.PaymentMethods.Select(p => PaymentMethodId.Parse(p)).ToArray(),
AutoApproveClaims = request.AutoApproveClaims
});
}
public async Task<string> CreatePullPayment(CreatePullPayment create)
{
ArgumentNullException.ThrowIfNull(create);
@ -281,7 +263,7 @@ namespace BTCPayServer.HostedServices
return await query.FirstOrDefaultAsync(data => data.Id == pullPaymentId);
}
record TopUpRequest(string PullPaymentId, InvoiceEntity InvoiceEntity);
class PayoutRequest
{
public PayoutRequest(TaskCompletionSource<ClaimRequest.ClaimResponse> completionSource,
@ -341,19 +323,8 @@ namespace BTCPayServer.HostedServices
{
payoutHandler.StartBackgroundCheck(Subscribe);
}
_eventAggregator.Subscribe<Events.InvoiceEvent>(TopUpInvoiceCore);
return new[] { Loop() };
}
private void TopUpInvoiceCore(InvoiceEvent evt)
{
if (evt.EventCode == InvoiceEventCode.Completed || evt.EventCode == InvoiceEventCode.MarkedCompleted)
{
foreach (var pullPaymentId in evt.Invoice.GetInternalTags("PULLPAY#"))
{
_Channel.Writer.TryWrite(new TopUpRequest(pullPaymentId, evt.Invoice));
}
}
return new[] { Loop() };
}
private void Subscribe(params Type[] events)
@ -368,11 +339,6 @@ namespace BTCPayServer.HostedServices
{
await foreach (var o in _Channel.Reader.ReadAllAsync())
{
if (o is TopUpRequest topUp)
{
await HandleTopUp(topUp);
}
if (o is PayoutRequest req)
{
await HandleCreatePayout(req);
@ -407,44 +373,10 @@ namespace BTCPayServer.HostedServices
}
}
private async Task HandleTopUp(TopUpRequest topUp)
{
var pp = await this.GetPullPayment(topUp.PullPaymentId, false);
var currency = pp.GetBlob().Currency;
using var ctx = _dbContextFactory.CreateContext();
var payout = new Data.PayoutData()
{
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20)),
Date = DateTimeOffset.UtcNow,
State = PayoutState.Completed,
PullPaymentDataId = pp.Id,
PaymentMethodId = new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToString(),
Destination = null,
StoreDataId = pp.StoreId
};
if (topUp.InvoiceEntity.Currency != currency ||
currency is not ("SATS" or "BTC"))
return;
var paidAmount = topUp.InvoiceEntity.Price;
var cryptoAmount = paidAmount;
var payoutBlob = new PayoutBlob()
{
CryptoAmount = -cryptoAmount,
Amount = -paidAmount,
Destination = topUp.InvoiceEntity.Id,
Metadata = new JObject(),
};
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
await ctx.Payouts.AddAsync(payout);
await ctx.SaveChangesAsync();
}
public bool SupportsLNURL(PullPaymentBlob blob)
{
var pms = blob.SupportedPaymentMethods.FirstOrDefault(id =>
id.PaymentType == LightningPaymentType.Instance &&
var pms = blob.SupportedPaymentMethods.FirstOrDefault(id =>
id.PaymentType == LightningPaymentType.Instance &&
_networkProvider.DefaultNetwork.CryptoCode == id.CryptoCode);
return pms is not null && _lnurlSupportedCurrencies.Contains(blob.Currency);
}
@ -701,7 +633,7 @@ namespace BTCPayServer.HostedServices
{
Amount = claimed,
Destination = req.ClaimRequest.Destination.ToString(),
Metadata = req.ClaimRequest.Metadata ?? new JObject(),
Metadata = req.ClaimRequest.Metadata?? new JObject(),
};
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
await ctx.Payouts.AddAsync(payout);
@ -894,10 +826,6 @@ namespace BTCPayServer.HostedServices
return time;
}
public static string GetInternalTag(string id)
{
return $"PULLPAY#{id}";
}
class InternalPayoutPaidRequest
{
@ -952,25 +880,25 @@ namespace BTCPayServer.HostedServices
{
null when destination.Amount is null && ppCurrency is null => ("Amount is not specified in destination or payout request", null),
null when destination.Amount is null => (null, null),
null when destination.Amount != null => (null, destination.Amount),
not null when destination.Amount is null => (null, amount),
null when destination.Amount != null => (null,destination.Amount),
not null when destination.Amount is null => (null,amount),
not null when destination.Amount != null && amount != destination.Amount &&
destination.IsExplicitAmountMinimum &&
payoutCurrency == "BTC" && ppCurrency == "SATS" &&
new Money(amount.Value, MoneyUnit.Satoshi).ToUnit(MoneyUnit.BTC) < destination.Amount =>
($"Amount is implied in both destination ({destination.Amount}) and payout request ({amount}), but the payout request amount is less than the destination amount", null),
($"Amount is implied in both destination ({destination.Amount}) and payout request ({amount}), but the payout request amount is less than the destination amount",null),
not null when destination.Amount != null && amount != destination.Amount &&
destination.IsExplicitAmountMinimum &&
!(payoutCurrency == "BTC" && ppCurrency == "SATS") &&
amount < destination.Amount =>
($"Amount is implied in both destination ({destination.Amount}) and payout request ({amount}), but the payout request amount is less than the destination amount", null),
($"Amount is implied in both destination ({destination.Amount}) and payout request ({amount}), but the payout request amount is less than the destination amount",null),
not null when destination.Amount != null && amount != destination.Amount &&
!destination.IsExplicitAmountMinimum =>
($"Amount is implied in destination ({destination.Amount}) that does not match the payout amount provided {amount})", null),
_ => (null, amount)
};
}
public static string GetErrorMessage(ClaimResult result)
{
switch (result)

View File

@ -8,167 +8,116 @@ 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;
public class UserEventHostedService(
EventAggregator eventAggregator,
UserManager<ApplicationUser> userManager,
EmailSenderFactory emailSenderFactory,
NotificationSender notificationSender,
StoreRepository storeRepository,
LinkGenerator generator,
Logs logs)
: EventHostedServiceBase(eventAggregator, logs)
namespace BTCPayServer.HostedServices
{
protected override void SubscribeToEvents()
public class UserEventHostedService : EventHostedServiceBase
{
Subscribe<UserRegisteredEvent>();
Subscribe<UserApprovedEvent>();
Subscribe<UserConfirmedEmailEvent>();
Subscribe<UserPasswordResetRequestedEvent>();
Subscribe<UserInviteAcceptedEvent>();
}
private readonly UserManager<ApplicationUser> _userManager;
private readonly EmailSenderFactory _emailSenderFactory;
private readonly NotificationSender _notificationSender;
private readonly LinkGenerator _generator;
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
{
string code;
string callbackUrl;
Uri uri;
HostString host;
ApplicationUser user;
IEmailSender emailSender;
switch (evt)
public UserEventHostedService(
EventAggregator eventAggregator,
UserManager<ApplicationUser> userManager,
EmailSenderFactory emailSenderFactory,
NotificationSender notificationSender,
LinkGenerator generator,
Logs logs) : base(eventAggregator, logs)
{
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;
_userManager = userManager;
_emailSenderFactory = emailSenderFactory;
_notificationSender = notificationSender;
_generator = generator;
}
}
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)
protected override void SubscribeToEvents()
{
emailSender.SendNewUserInfo(admin.GetMailboxAddress(), newUserInfo, approvalLink);
Subscribe<UserRegisteredEvent>();
Subscribe<UserApprovedEvent>();
Subscribe<UserPasswordResetRequestedEvent>();
}
}
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)
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
{
// 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)
string code;
string callbackUrl;
Uri uri;
HostString host;
ApplicationUser user;
MailboxAddress address;
IEmailSender emailSender;
UserPasswordResetRequestedEvent userPasswordResetRequestedEvent;
switch (evt)
{
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);
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;
}
}
}

View File

@ -1,8 +1,11 @@
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;
@ -63,9 +66,11 @@ 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;
@ -304,7 +309,6 @@ namespace BTCPayServer.Hosting
});
services.TryAddSingleton<BTCPayNetworkProvider>();
services.AddExceptionHandler<PluginExceptionHandler>();
services.TryAddSingleton<AppService>();
services.AddTransient<PluginService>();
services.AddSingleton<PluginHookService>();
@ -348,8 +352,6 @@ namespace BTCPayServer.Hosting
htmlSanitizer.AllowedAttributes.Add("webkitallowfullscreen");
htmlSanitizer.AllowedAttributes.Add("allowfullscreen");
htmlSanitizer.AllowedSchemes.Add("mailto");
htmlSanitizer.AllowedSchemes.Add("bitcoin");
htmlSanitizer.AllowedSchemes.Add("lightning");
return htmlSanitizer;
});
@ -388,7 +390,7 @@ namespace BTCPayServer.Hosting
services.AddReportProvider<ProductsReportProvider>();
services.AddReportProvider<PayoutsReportProvider>();
services.AddReportProvider<LegacyInvoiceExportReportProvider>();
services.AddReportProvider<RefundsReportProvider>();
services.AddReportProvider<RefundReportProvider>();
services.AddWebhooks();
services.AddSingleton<BitcoinLikePayoutHandler>();
services.AddSingleton<IPayoutHandler>(provider => provider.GetRequiredService<BitcoinLikePayoutHandler>());
@ -436,7 +438,6 @@ 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>();

View File

@ -88,8 +88,7 @@ namespace BTCPayServer.Hosting
.PersistKeysToFileSystem(new DirectoryInfo(new DataDirectories().Configure(Configuration).DataDir));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
.AddInvitationTokenProvider();
.AddDefaultTokenProviders();
services.Configure<AuthenticationOptions>(opts =>
{
opts.DefaultAuthenticateScheme = null;
@ -293,7 +292,6 @@ namespace BTCPayServer.Hosting
app.UseStatusCodePagesWithReExecute("/errors/{0}");
app.UseExceptionHandler("/errors/{0}");
app.UsePayServer();
app.UseRouting();
app.UseCors();

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