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 30a051f
Show file tree
Hide file tree
Showing 30 changed files with 355 additions and 192 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)
{
}
}
}
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
120 changes: 80 additions & 40 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 @@ -1640,7 +1641,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 +1661,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 +2513,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.ServerUri + "LocalStorage/8f890691-87f9-4c65-80e5-3b7ffaa3551f-store.png", vm.LogoUrl);
Assert.Equal(tester.PayTester.ServerUri + "LocalStorage/2a51c49a-9d54-4013-80a2-3f6e69d08523-store.css", vm.CssUrl);

var vm2 = await controller.CheckoutAppearance().AssertViewModelAsync<CheckoutAppearanceViewModel>();
Assert.Equal(tester.PayTester.ServerUri + "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.ServerUri + "LocalStorage/ce71d90a-dd90-40a3-b1f0-96d00c9abb52-admin.png", branding.LogoUrl);
Assert.Equal(tester.PayTester.ServerUri + "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 = UnresolvedUri.Create(restModel.LogoUrl);
blob.CssUrl = UnresolvedUri.Create(restModel.CssUrl);
if (restModel.AutoDetectLanguage.HasValue)
blob.AutoDetectLanguage = restModel.AutoDetectLanguage.Value;
if (restModel.ShowPayInWalletButton.HasValue)
Expand Down
8 changes: 4 additions & 4 deletions BTCPayServer/Controllers/UIInvoiceController.UI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ public async Task<IActionResult> InvoiceReceipt(string invoiceId, [FromQuery] bo
Currency = i.Currency,
Timestamp = i.InvoiceTime,
StoreName = store.StoreName,
StoreBranding = new StoreBrandingViewModel(storeBlob),
StoreBranding = await StoreBrandingViewModel.CreateAsync(Request, _uriResolver, storeBlob),
ReceiptOptions = receipt
};

Expand Down Expand Up @@ -889,7 +889,7 @@ string GetPaymentMethodImage(PaymentMethodId paymentMethodId)
DefaultLang = lang ?? invoice.DefaultLanguage ?? storeBlob.DefaultLang ?? "en",
ShowPayInWalletButton = storeBlob.ShowPayInWalletButton,
ShowStoreHeader = storeBlob.ShowStoreHeader,
StoreBranding = new StoreBrandingViewModel(storeBlob),
StoreBranding = await StoreBrandingViewModel.CreateAsync(Request, _uriResolver, storeBlob),
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
CelebratePayment = storeBlob.CelebratePayment,
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback,
Expand Down Expand Up @@ -978,9 +978,9 @@ string GetPaymentMethodImage(PaymentMethodId paymentMethodId)

if (storeBlob.PlaySoundOnPayment)
{
model.PaymentSoundUrl = string.IsNullOrEmpty(storeBlob.PaymentSoundUrl)
model.PaymentSoundUrl = storeBlob.PaymentSoundUrl is null
? string.Concat(Request.GetAbsoluteRootUri().ToString(), "checkout/payment.mp3")
: storeBlob.PaymentSoundUrl;
: await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), storeBlob.PaymentSoundUrl);
model.ErrorSoundUrl = string.Concat(Request.GetAbsoluteRootUri().ToString(), "checkout/error.mp3");
model.NfcReadSoundUrl = string.Concat(Request.GetAbsoluteRootUri().ToString(), "checkout/nfcread.mp3");
}
Expand Down
3 changes: 3 additions & 0 deletions BTCPayServer/Controllers/UIInvoiceController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public partial class UIInvoiceController : Controller
private readonly PaymentMethodViewProvider _viewProvider;
private readonly AppService _appService;
private readonly IFileService _fileService;
private readonly UriResolver _uriResolver;

public WebhookSender WebhookNotificationManager { get; }

Expand All @@ -91,6 +92,7 @@ public partial class UIInvoiceController : Controller
LinkGenerator linkGenerator,
AppService appService,
IFileService fileService,
UriResolver uriResolver,
IAuthorizationService authorizationService,
TransactionLinkProviders transactionLinkProviders,
Dictionary<PaymentMethodId, IPaymentModelExtension> paymentModelExtensions,
Expand Down Expand Up @@ -120,6 +122,7 @@ public partial class UIInvoiceController : Controller
_paymentModelExtensions = paymentModelExtensions;
_viewProvider = viewProvider;
_fileService = fileService;
_uriResolver = uriResolver;
_appService = appService;
}

Expand Down

0 comments on commit 30a051f

Please sign in to comment.