Skip to content

Commit

Permalink
Store and Server branding can reference file's via fileid:ID
Browse files Browse the repository at this point in the history
  • Loading branch information
NicolasDorier committed May 8, 2024
1 parent a313f07 commit b069c38
Show file tree
Hide file tree
Showing 32 changed files with 365 additions and 198 deletions.
61 changes: 61 additions & 0 deletions BTCPayServer.Data/Migrations/20240508015052_fileid.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240508015052_fileid")]
public partial class fileid : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
UPDATE "Settings"
SET "Value" = jsonb_set(
"Value",
'{LogoUrl}',
to_jsonb('fileid:' || ("Value"->>'LogoFileId'))) - 'LogoFileId'
WHERE "Id" = 'BTCPayServer.Services.ThemeSettings'
AND "Value"->>'LogoFileId' IS NOT NULL;

UPDATE "Settings"
SET "Value" = jsonb_set(
"Value",
'{CustomThemeCssUrl}',
to_jsonb('fileid:' || ("Value"->>'CustomThemeFileId'))) - 'CustomThemeFileId'
WHERE "Id" = 'BTCPayServer.Services.ThemeSettings'
AND "Value"->>'CustomThemeFileId' IS NOT NULL;

UPDATE "Stores"
SET "StoreBlob" = jsonb_set(
"StoreBlob",
'{logoUrl}',
to_jsonb('fileid:' || ("StoreBlob"->>'logoFileId'))) - 'logoFileId'
WHERE "StoreBlob"->>'logoFileId' IS NOT NULL;

UPDATE "Stores"
SET "StoreBlob" = jsonb_set(
"StoreBlob",
'{cssUrl}',
to_jsonb('fileid:' || ("StoreBlob"->>'cssFileId'))) - 'cssFileId'
WHERE "StoreBlob"->>'cssFileId' IS NOT NULL;

UPDATE "Stores"
SET "StoreBlob" = jsonb_set(
"StoreBlob",
'{paymentSoundUrl}',
to_jsonb('fileid:' || ("StoreBlob"->>'soundFileId'))) - 'soundFileId'
WHERE "StoreBlob"->>'soundFileId' IS NOT NULL;
""");
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
6 changes: 6 additions & 0 deletions BTCPayServer.Tests/BTCPayServerTester.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ public Uri ServerUri
get;
set;
}
public Uri ServerUriWithIP
{
get;
set;
}

public string MySQL
{
Expand Down Expand Up @@ -164,6 +169,7 @@ public async Task StartAsync()
await File.WriteAllTextAsync(confPath, config.ToString());

ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
ServerUriWithIP = new Uri("http://127.0.0.1:" + Port + "/");
HttpClient = new HttpClient();
HttpClient.BaseAddress = ServerUri;
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
Expand Down
2 changes: 1 addition & 1 deletion BTCPayServer.Tests/TestAccount.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public async Task SetNetworkFeeMode(NetworkFeeMode mode)
public async Task ModifyPayment(Action<GeneralSettingsViewModel> modify)
{
var storeController = GetController<UIStoresController>();
var response = storeController.GeneralSettings();
var response = await storeController.GeneralSettings();
GeneralSettingsViewModel settings = (GeneralSettingsViewModel)((ViewResult)response).Model;
modify(settings);
await storeController.GeneralSettings(settings);
Expand Down
126 changes: 82 additions & 44 deletions BTCPayServer.Tests/UnitTest1.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using Dapper;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
Expand Down Expand Up @@ -53,7 +54,6 @@
using BTCPayServer.Storage.Models;
using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration;
using BTCPayServer.Storage.ViewModels;
using ExchangeSharp;
using Fido2NetLib;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -79,6 +79,7 @@
using MarkPayoutRequest = BTCPayServer.Client.Models.MarkPayoutRequest;
using PaymentRequestData = BTCPayServer.Client.Models.PaymentRequestData;
using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel;
using Microsoft.Extensions.Caching.Memory;

namespace BTCPayServer.Tests
{
Expand Down Expand Up @@ -297,7 +298,7 @@ public async Task CanAcceptInvoiceWithTolerance2()

// Set tolerance to 50%
var stores = user.GetController<UIStoresController>();
var response = stores.GeneralSettings();
var response = await stores.GeneralSettings();
var vm = Assert.IsType<GeneralSettingsViewModel>(Assert.IsType<ViewResult>(response).Model);
Assert.Equal(0.0, vm.PaymentTolerance);
vm.PaymentTolerance = 50.0;
Expand Down Expand Up @@ -439,7 +440,7 @@ public async Task CanSetLightningServer()
var user = tester.NewAccount();
await user.GrantAccessAsync(true);
var storeController = user.GetController<UIStoresController>();
var storeResponse = storeController.GeneralSettings();
var storeResponse = await storeController.GeneralSettings();
Assert.IsType<ViewResult>(storeResponse);
Assert.IsType<ViewResult>(storeController.SetupLightningNode(user.StoreId, "BTC"));

Expand Down Expand Up @@ -838,7 +839,7 @@ public async Task CanListInvoices()

var time = invoice.InvoiceTime;
AssertSearchInvoice(acc, true, invoice.Id, $"startdate:{time.ToString("yyyy-MM-dd HH:mm:ss")}");
AssertSearchInvoice(acc, true, invoice.Id, $"enddate:{time.ToStringLowerInvariant()}");
AssertSearchInvoice(acc, true, invoice.Id, $"enddate:{time.ToString().ToLowerInvariant()}");
AssertSearchInvoice(acc, false, invoice.Id,
$"startdate:{time.AddSeconds(1).ToString("yyyy-MM-dd HH:mm:ss")}");
AssertSearchInvoice(acc, false, invoice.Id,
Expand Down Expand Up @@ -1499,8 +1500,7 @@ public async Task CanSetPaymentMethodLimits()
var btcMethod = PaymentTypes.CHAIN.GetPaymentMethodId("BTC").ToString();

// We allow BTC and LN, but not BTC under 5 USD, so only LN should be in the invoice
var vm = Assert.IsType<CheckoutAppearanceViewModel>(Assert
.IsType<ViewResult>(user.GetController<UIStoresController>().CheckoutAppearance()).Model);
var vm = await user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModelAsync<CheckoutAppearanceViewModel>();
Assert.Equal(2, vm.PaymentMethodCriteria.Count);
var criteria = Assert.Single(vm.PaymentMethodCriteria.Where(m => m.PaymentMethod == btcMethod.ToString()));
Assert.Equal(PaymentTypes.CHAIN.GetPaymentMethodId("BTC").ToString(), criteria.PaymentMethod);
Expand All @@ -1527,8 +1527,7 @@ public async Task CanSetPaymentMethodLimits()
// 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
// payment method should be LN.
vm = Assert.IsType<CheckoutAppearanceViewModel>(Assert
.IsType<ViewResult>(user.GetController<UIStoresController>().CheckoutAppearance()).Model);
vm = await user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModelAsync<CheckoutAppearanceViewModel>();
vm.DefaultPaymentMethod = lnMethod;
criteria = vm.PaymentMethodCriteria.First();
criteria.Value = "150 USD";
Expand Down Expand Up @@ -1640,7 +1639,7 @@ public async Task CanSetPaymentMethodLimitsLightning()
user.GrantAccess(true);
user.RegisterLightningNode(cryptoCode);
user.SetLNUrl(cryptoCode, false);
var vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
var vm = await user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModelAsync<CheckoutAppearanceViewModel>();
var criteria = Assert.Single(vm.PaymentMethodCriteria);
Assert.Equal(PaymentTypes.LN.GetPaymentMethodId(cryptoCode).ToString(), criteria.PaymentMethod);
criteria.Value = "2 USD";
Expand All @@ -1660,7 +1659,7 @@ public async Task CanSetPaymentMethodLimitsLightning()
// Activating LNUrl, we should still have only 1 payment criteria that can be set.
user.RegisterLightningNode(cryptoCode);
user.SetLNUrl(cryptoCode, true);
vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
vm = await user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModelAsync<CheckoutAppearanceViewModel>();
criteria = Assert.Single(vm.PaymentMethodCriteria);
Assert.Equal(PaymentTypes.LN.GetPaymentMethodId(cryptoCode).ToString(), criteria.PaymentMethod);
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm).Result);
Expand Down Expand Up @@ -2512,44 +2511,83 @@ public async Task CanFixMappedDomainAppType()
public async Task CanMigrateFileIds()
{
using var tester = CreateServerTester(newDb: true);
tester.DeleteStore = false;
await tester.StartAsync();
var f = tester.PayTester.GetService<ApplicationDbContextFactory>();

var user = tester.NewAccount();
await user.GrantAccessAsync();

// upload file to get a working fileId
var controller = tester.PayTester.GetController<UIServerController>(user.UserId, user.StoreId);
Assert.IsType<FileSystemStorageConfiguration>(Assert.IsType<ViewResult>(await controller.StorageProvider(StorageProvider.FileSystem.ToString())).Model);
var fileId = await CanUploadFile(controller);

// attach file id as logo to store
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
var blob = store!.GetStoreBlob();
blob.LogoFileId = fileId;
blob.LogoUrl = null;
blob.CssFileId = null;
store.SetStoreBlob(blob);
await tester.PayTester.StoreRepository.UpdateStore(store);

// create legacy theme setting that needs migration
var settingsRepo = tester.PayTester.GetService<SettingsRepository>();
await settingsRepo.UpdateSetting(new ThemeSettings { LogoFileId = fileId, CustomThemeFileId = null });

// migrate and check
await RestartMigration(tester);
var settings = await settingsRepo.GetSettingAsync<ThemeSettings>();
Assert.NotNull(settings);
Assert.Null(settings.LogoFileId);
Assert.Null(settings.CustomThemeFileId);
Assert.Null(settings.CustomThemeCssUrl);
Assert.StartsWith("/LocalStorage/", settings.LogoUrl);

store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
blob = store!.GetStoreBlob();
Assert.Null(blob.CssFileId);
Assert.Null(blob.LogoFileId);
Assert.Null(blob.CssUrl);
Assert.StartsWith("/LocalStorage/", blob.LogoUrl);
using (var ctx = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext())
{
var storeConfig = """
{
"spread": 0.0,
"cssFileId": "2a51c49a-9d54-4013-80a2-3f6e69d08523",
"logoFileId": "8f890691-87f9-4c65-80e5-3b7ffaa3551f",
"soundFileId": "62bc4757-b92b-4a3b-a8ab-0e9b693d6a29",
"networkFeeMode": "MultiplePaymentsOnly",
"defaultCurrency": "USD",
"showStoreHeader": true,
"celebratePayment": true,
"paymentTolerance": 0.0,
"invoiceExpiration": 15,
"preferredExchange": "kraken",
"showRecommendedFee": true,
"monitoringExpiration": 1440,
"showPayInWalletButton": true,
"displayExpirationTimer": 5,
"excludedPaymentMethods": null,
"recommendedFeeBlockTarget": 1
}
""";
var serverConfig = """
{
"CssUri": null,
"FirstRun": false,
"LogoFileId": "ce71d90a-dd90-40a3-b1f0-96d00c9abb52",
"CustomTheme": true,
"CustomThemeCssUri": null,
"CustomThemeFileId": "9b00f4ed-914b-437b-abd2-9a90c1b22c34",
"CustomThemeExtension": 0
}
""";
await ctx.Database.GetDbConnection().ExecuteAsync("""
UPDATE "Stores" SET "StoreBlob"=@storeConfig::JSONB WHERE "Id"=@storeId;
""", new { storeId = user.StoreId, storeConfig });
await ctx.Database.GetDbConnection().ExecuteAsync("""
UPDATE "Settings" SET "Value"=@serverConfig::JSONB WHERE "Id"='BTCPayServer.Services.ThemeSettings';
""", new { serverConfig });
await ctx.Database.GetDbConnection().ExecuteAsync("""
INSERT INTO "Files" VALUES (@id, @fileName, @id || '-' || @fileName, NOW(), @userId);
""",
new[]
{
new { id = "2a51c49a-9d54-4013-80a2-3f6e69d08523", fileName = "store.css", userId = user.UserId },
new { id = "8f890691-87f9-4c65-80e5-3b7ffaa3551f", fileName = "store.png", userId = user.UserId },
new { id = "ce71d90a-dd90-40a3-b1f0-96d00c9abb52", fileName = "admin.png", userId = user.UserId },
new { id = "9b00f4ed-914b-437b-abd2-9a90c1b22c34", fileName = "admin.css", userId = user.UserId },
new { id = "62bc4757-b92b-4a3b-a8ab-0e9b693d6a29", fileName = "store.mp3", userId = user.UserId },
});
await ctx.Database.GetDbConnection().ExecuteAsync("""
DELETE FROM "__EFMigrationsHistory" WHERE "MigrationId"='20240508015052_fileid'
""");
await ctx.Database.MigrateAsync();
((MemoryCache)tester.PayTester.GetService<IMemoryCache>()).Clear();
}

var controller = tester.PayTester.GetController<UIStoresController>(user.UserId, user.StoreId);
var vm = await controller.GeneralSettings().AssertViewModelAsync<GeneralSettingsViewModel>();
Assert.Equal(tester.PayTester.ServerUriWithIP + "LocalStorage/8f890691-87f9-4c65-80e5-3b7ffaa3551f-store.png", vm.LogoUrl);
Assert.Equal(tester.PayTester.ServerUriWithIP + "LocalStorage/2a51c49a-9d54-4013-80a2-3f6e69d08523-store.css", vm.CssUrl);

var vm2 = await controller.CheckoutAppearance().AssertViewModelAsync<CheckoutAppearanceViewModel>();
Assert.Equal(tester.PayTester.ServerUriWithIP + "LocalStorage/62bc4757-b92b-4a3b-a8ab-0e9b693d6a29-store.mp3", vm2.PaymentSoundUrl);

var serverController = tester.PayTester.GetController<UIServerController>();
var branding = await serverController.Branding().AssertViewModelAsync<BrandingViewModel>();

Assert.Equal(tester.PayTester.ServerUriWithIP + "LocalStorage/ce71d90a-dd90-40a3-b1f0-96d00c9abb52-admin.png", branding.LogoUrl);
Assert.Equal(tester.PayTester.ServerUriWithIP + "LocalStorage/9b00f4ed-914b-437b-abd2-9a90c1b22c34-admin.css", branding.CustomThemeCssUrl);
}

[Fact(Timeout = LongRunningTestTimeout)]
Expand Down
6 changes: 4 additions & 2 deletions BTCPayServer/Components/MainLogo/Default.cshtml
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
@using BTCPayServer.Services
@inject ThemeSettings Theme
@inject UriResolver UriResolver
@model BTCPayServer.Components.MainLogo.MainLogoViewModel

@if (!string.IsNullOrEmpty(Theme.LogoUrl))
@if (Theme.LogoUrl is not null)
{
<img src="@Theme.LogoUrl" alt="BTCPay Server" class="main-logo main-logo-custom @Model.CssClass" />
var logoUrl = await UriResolver.Resolve(this.Context.Request.GetAbsoluteRootUri(), Theme.LogoUrl);
<img src="@logoUrl" alt="BTCPay Server" class="main-logo main-logo-custom @Model.CssClass" />
}
else
{
Expand Down
8 changes: 7 additions & 1 deletion BTCPayServer/Components/StoreSelector/StoreSelector.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
Expand All @@ -14,13 +17,16 @@ namespace BTCPayServer.Components.StoreSelector
public class StoreSelector : ViewComponent
{
private readonly StoreRepository _storeRepo;
private readonly UriResolver _uriResolver;
private readonly UserManager<ApplicationUser> _userManager;

public StoreSelector(
StoreRepository storeRepo,
UriResolver uriResolver,
UserManager<ApplicationUser> userManager)
{
_storeRepo = storeRepo;
_uriResolver = uriResolver;
_userManager = userManager;
}

Expand Down Expand Up @@ -50,7 +56,7 @@ public async Task<IViewComponentResult> InvokeAsync()
Options = options,
CurrentStoreId = currentStore?.Id,
CurrentDisplayName = currentStore?.StoreName,
CurrentStoreLogoUrl = blob?.LogoUrl,
CurrentStoreLogoUrl = await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), blob?.LogoUrl),
ArchivedCount = archivedCount
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ internal static Client.Models.StoreData FromModel(StoreData data)
Website = data.StoreWebsite,
Archived = data.Archived,
BrandColor = storeBlob.BrandColor,
CssUrl = storeBlob.CssUrl,
LogoUrl = storeBlob.LogoUrl,
CssUrl = storeBlob.CssUrl?.ToString(),
LogoUrl = storeBlob.LogoUrl?.ToString(),
SupportUrl = storeBlob.StoreSupportUrl,
SpeedPolicy = data.SpeedPolicy,
DefaultPaymentMethod = data.GetDefaultPaymentId()?.ToString(),
Expand Down Expand Up @@ -196,8 +196,8 @@ private void ToModel(StoreBaseData restModel, StoreData model, PaymentMethodId d
blob.PaymentTolerance = restModel.PaymentTolerance;
blob.PayJoinEnabled = restModel.PayJoinEnabled;
blob.BrandColor = restModel.BrandColor;
blob.LogoUrl = restModel.LogoUrl;
blob.CssUrl = restModel.CssUrl;
blob.LogoUrl = restModel.LogoUrl is null ? null : UnresolvedUri.Create(restModel.LogoUrl);
blob.CssUrl = restModel.CssUrl is null ? null : UnresolvedUri.Create(restModel.CssUrl);
if (restModel.AutoDetectLanguage.HasValue)
blob.AutoDetectLanguage = restModel.AutoDetectLanguage.Value;
if (restModel.ShowPayInWalletButton.HasValue)
Expand Down

0 comments on commit b069c38

Please sign in to comment.