mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 01:43:50 +01:00
Store Settings feature with own table (#3851)
* Store Settings feature with own table * fix test * Include the store settings to StoreRepository, remove caching stuff Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
parent
4691e896a1
commit
9a24e4ade1
10
BTCPayServer.Abstractions/Contracts/IStoreRepository.cs
Normal file
10
BTCPayServer.Abstractions/Contracts/IStoreRepository.cs
Normal file
@ -0,0 +1,10 @@
|
||||
#nullable enable
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Contracts;
|
||||
|
||||
public interface IStoreRepository
|
||||
{
|
||||
Task<T?> GetSettingAsync<T>(string storeId, string name) where T : class;
|
||||
Task UpdateSetting<T>(string storeId, string name, T obj) where T : class;
|
||||
}
|
@ -53,6 +53,7 @@ namespace BTCPayServer.Data
|
||||
public DbSet<PullPaymentData> PullPayments { get; set; }
|
||||
public DbSet<RefundData> Refunds { get; set; }
|
||||
public DbSet<SettingData> Settings { get; set; }
|
||||
public DbSet<StoreSettingData> StoreSettings { get; set; }
|
||||
public DbSet<StoreWebhookData> StoreWebhooks { get; set; }
|
||||
public DbSet<StoreData> Stores { get; set; }
|
||||
public DbSet<U2FDevice> U2FDevices { get; set; }
|
||||
@ -101,6 +102,7 @@ namespace BTCPayServer.Data
|
||||
PullPaymentData.OnModelCreating(builder);
|
||||
RefundData.OnModelCreating(builder);
|
||||
//SettingData.OnModelCreating(builder);
|
||||
StoreSettingData.OnModelCreating(builder, Database);
|
||||
StoreWebhookData.OnModelCreating(builder);
|
||||
//StoreData.OnModelCreating(builder);
|
||||
U2FDevice.OnModelCreating(builder);
|
||||
|
@ -47,5 +47,6 @@ namespace BTCPayServer.Data
|
||||
public IEnumerable<PayoutProcessorData> PayoutProcessors { get; set; }
|
||||
public IEnumerable<PayoutData> Payouts { get; set; }
|
||||
public IEnumerable<CustodianAccountData> CustodianAccounts { get; set; }
|
||||
public IEnumerable<StoreSettingData> Settings { get; set; }
|
||||
}
|
||||
}
|
||||
|
28
BTCPayServer.Data/Data/StoreSettingData.cs
Normal file
28
BTCPayServer.Data/Data/StoreSettingData.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace BTCPayServer.Data;
|
||||
|
||||
public class StoreSettingData
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
|
||||
public string Value { get; set; }
|
||||
|
||||
public StoreData Store { get; set; }
|
||||
|
||||
public static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
builder.Entity<StoreSettingData>().HasKey(data => new { data.StoreId, data.Name });
|
||||
builder.Entity<StoreSettingData>()
|
||||
.HasOne(o => o.Store)
|
||||
.WithMany(o => o.Settings).OnDelete(DeleteBehavior.Cascade);
|
||||
if (databaseFacade.IsNpgsql())
|
||||
{
|
||||
builder.Entity<StoreSettingData>()
|
||||
.Property(o => o.Value)
|
||||
.HasColumnType("JSONB");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20220610090843_AddSettingsToStore")]
|
||||
public partial class AddSettingsToStore : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "StoreSettings",
|
||||
columns: table => new
|
||||
{
|
||||
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||
StoreId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Value = table.Column<string>(type: migrationBuilder.IsNpgsql() ? "JSONB" : "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_StoreSettings", x => new { x.StoreId, x.Name });
|
||||
table.ForeignKey(
|
||||
name: "FK_StoreSettings_Stores_StoreId",
|
||||
column: x => x.StoreId,
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "StoreSettings");
|
||||
}
|
||||
}
|
||||
}
|
@ -746,6 +746,22 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreSettingData", b =>
|
||||
{
|
||||
b.Property<string>("StoreId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("StoreId", "Name");
|
||||
|
||||
b.ToTable("StoreSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreWebhookData", b =>
|
||||
{
|
||||
b.Property<string>("StoreId")
|
||||
@ -1258,6 +1274,17 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("ApplicationUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreSettingData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
||||
.WithMany("Settings")
|
||||
.HasForeignKey("StoreId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Store");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreWebhookData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
||||
@ -1438,6 +1465,8 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
b.Navigation("PullPayments");
|
||||
|
||||
b.Navigation("Settings");
|
||||
|
||||
b.Navigation("UserStores");
|
||||
});
|
||||
|
||||
|
@ -186,6 +186,37 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Assert.Equal(description, json["components"]["securitySchemes"]["API_Key"]["description"].Value<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanStoreArbitrarySettingsWithStore()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
var settingsRepo = tester.PayTester.ServiceProvider.GetRequiredService<IStoreRepository>();
|
||||
var arbValue = await settingsRepo.GetSettingAsync<string>(user.StoreId,"arbitrary");
|
||||
Assert.Null(arbValue);
|
||||
await settingsRepo.UpdateSetting(user.StoreId, "arbitrary", "saved");
|
||||
|
||||
arbValue = await settingsRepo.GetSettingAsync<string>(user.StoreId,"arbitrary");
|
||||
Assert.Equal("saved", arbValue);
|
||||
|
||||
await settingsRepo.UpdateSetting<TestData>(user.StoreId, "arbitrary", new TestData() { Name = "hello" });
|
||||
var arbData = await settingsRepo.GetSettingAsync<TestData>(user.StoreId, "arbitrary");
|
||||
Assert.Equal("hello", arbData.Name);
|
||||
|
||||
var client = await user.CreateClient();
|
||||
await client.RemoveStore(user.StoreId);
|
||||
tester.Stores.Clear();
|
||||
arbValue = await settingsRepo.GetSettingAsync<string>(user.StoreId, "arbitrary");
|
||||
Assert.Null(arbValue);
|
||||
}
|
||||
class TestData
|
||||
{
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
private async Task CheckDeadLinks(Regex regex, HttpClient httpClient, string file)
|
||||
{
|
||||
|
@ -4,6 +4,8 @@ namespace BTCPayServer.Events
|
||||
{
|
||||
public string SettingsName { get; set; }
|
||||
public T Settings { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Settings {typeof(T).Name} changed";
|
||||
|
@ -76,6 +76,7 @@ namespace BTCPayServer.Hosting
|
||||
public static IServiceCollection AddBTCPayServer(this IServiceCollection services, IConfiguration configuration, Logs logs)
|
||||
{
|
||||
services.AddSingleton<MvcNewtonsoftJsonOptions>(o => o.GetRequiredService<IOptions<MvcNewtonsoftJsonOptions>>().Value);
|
||||
services.AddSingleton<JsonSerializerSettings>(o => o.GetRequiredService<IOptions<MvcNewtonsoftJsonOptions>>().Value.SerializerSettings);
|
||||
services.AddDbContext<ApplicationDbContext>((provider, o) =>
|
||||
{
|
||||
var factory = provider.GetRequiredService<ApplicationDbContextFactory>();
|
||||
@ -99,6 +100,7 @@ namespace BTCPayServer.Hosting
|
||||
#endif
|
||||
services.TryAddSingleton<SettingsRepository>();
|
||||
services.TryAddSingleton<ISettingsRepository>(provider => provider.GetService<SettingsRepository>());
|
||||
services.TryAddSingleton<IStoreRepository>(provider => provider.GetService<StoreRepository>());
|
||||
services.TryAddSingleton<LabelFactory>();
|
||||
services.TryAddSingleton<TorServices>();
|
||||
services.AddSingleton<IHostedService>(provider => provider.GetRequiredService<TorServices>());
|
||||
|
@ -1,28 +1,35 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Migrations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Stores
|
||||
{
|
||||
public class StoreRepository
|
||||
public class StoreRepository : IStoreRepository
|
||||
{
|
||||
private readonly ApplicationDbContextFactory _ContextFactory;
|
||||
|
||||
public JsonSerializerSettings SerializerSettings { get; }
|
||||
|
||||
public ApplicationDbContext CreateDbContext()
|
||||
{
|
||||
return _ContextFactory.CreateContext();
|
||||
}
|
||||
public StoreRepository(ApplicationDbContextFactory contextFactory)
|
||||
public StoreRepository(ApplicationDbContextFactory contextFactory, JsonSerializerSettings serializerSettings)
|
||||
{
|
||||
_ContextFactory = contextFactory ?? throw new ArgumentNullException(nameof(contextFactory));
|
||||
SerializerSettings = serializerSettings;
|
||||
}
|
||||
|
||||
public async Task<StoreData> FindStore(string storeId)
|
||||
public async Task<StoreData?> FindStore(string storeId)
|
||||
{
|
||||
if (storeId == null)
|
||||
return null;
|
||||
@ -31,7 +38,7 @@ namespace BTCPayServer.Services.Stores
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<StoreData> FindStore(string storeId, string userId)
|
||||
public async Task<StoreData?> FindStore(string storeId, string userId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userId);
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
@ -50,13 +57,14 @@ namespace BTCPayServer.Services.Stores
|
||||
return us.Store;
|
||||
}).FirstOrDefault();
|
||||
}
|
||||
|
||||
#nullable disable
|
||||
public class StoreUser
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string Role { get; set; }
|
||||
}
|
||||
#nullable enable
|
||||
public async Task<StoreUser[]> GetStoreUsers(string storeId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(storeId);
|
||||
@ -72,7 +80,7 @@ namespace BTCPayServer.Services.Stores
|
||||
}).ToArrayAsync();
|
||||
}
|
||||
|
||||
public async Task<StoreData[]> GetStoresByUserId(string userId, IEnumerable<string> storeIds = null)
|
||||
public async Task<StoreData[]> GetStoresByUserId(string userId, IEnumerable<string>? storeIds = null)
|
||||
{
|
||||
using var ctx = _ContextFactory.CreateContext();
|
||||
return (await ctx.UserStore
|
||||
@ -86,7 +94,7 @@ namespace BTCPayServer.Services.Stores
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public async Task<StoreData> GetStoreByInvoiceId(string invoiceId)
|
||||
public async Task<StoreData?> GetStoreByInvoiceId(string invoiceId)
|
||||
{
|
||||
await using var context = _ContextFactory.CreateContext();
|
||||
var matched = await context.Invoices.Include(data => data.StoreData)
|
||||
@ -152,7 +160,6 @@ namespace BTCPayServer.Services.Stores
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateStore(string ownerId, StoreData storeData)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(storeData.Id))
|
||||
@ -194,7 +201,7 @@ namespace BTCPayServer.Services.Stores
|
||||
.Select(s => s.Webhook).ToArrayAsync();
|
||||
}
|
||||
|
||||
public async Task<WebhookDeliveryData> GetWebhookDelivery(string storeId, string webhookId, string deliveryId)
|
||||
public async Task<WebhookDeliveryData?> GetWebhookDelivery(string storeId, string webhookId, string deliveryId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(webhookId);
|
||||
ArgumentNullException.ThrowIfNull(storeId);
|
||||
@ -256,7 +263,7 @@ namespace BTCPayServer.Services.Stores
|
||||
return data.Id;
|
||||
}
|
||||
|
||||
public async Task<WebhookData> GetWebhook(string storeId, string webhookId)
|
||||
public async Task<WebhookData?> GetWebhook(string storeId, string webhookId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(webhookId);
|
||||
ArgumentNullException.ThrowIfNull(storeId);
|
||||
@ -266,7 +273,7 @@ namespace BTCPayServer.Services.Stores
|
||||
.Select(s => s.Webhook)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
public async Task<WebhookData> GetWebhook(string webhookId)
|
||||
public async Task<WebhookData?> GetWebhook(string webhookId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(webhookId);
|
||||
using var ctx = _ContextFactory.CreateContext();
|
||||
@ -323,8 +330,11 @@ namespace BTCPayServer.Services.Stores
|
||||
{
|
||||
using var ctx = _ContextFactory.CreateContext();
|
||||
var existing = await ctx.FindAsync<StoreData>(store.Id);
|
||||
ctx.Entry(existing).CurrentValues.SetValues(store);
|
||||
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
||||
if (existing is not null)
|
||||
{
|
||||
ctx.Entry(existing).CurrentValues.SetValues(store);
|
||||
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteStore(string storeId)
|
||||
@ -357,6 +367,51 @@ namespace BTCPayServer.Services.Stores
|
||||
return true;
|
||||
}
|
||||
|
||||
private T? Deserialize<T>(string value) where T : class
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(value, SerializerSettings);
|
||||
}
|
||||
|
||||
private string Serialize<T>(T obj)
|
||||
{
|
||||
return JsonConvert.SerializeObject(obj, SerializerSettings);
|
||||
}
|
||||
public async Task<T?> GetSettingAsync<T>(string storeId, string name) where T : class
|
||||
{
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
var data = await ctx.StoreSettings.Where(s => s.Name == name && s.StoreId == storeId).FirstOrDefaultAsync();
|
||||
return data == null ? default : this.Deserialize<T>(data.Value);
|
||||
|
||||
}
|
||||
|
||||
public async Task UpdateSetting<T>(string storeId, string name, T obj) where T : class
|
||||
{
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
StoreSettingData? settings = null;
|
||||
if (obj is null)
|
||||
{
|
||||
ctx.StoreSettings.RemoveRange(ctx.StoreSettings.Where(data => data.Name == name && data.StoreId == storeId));
|
||||
}
|
||||
else
|
||||
{
|
||||
settings = new StoreSettingData() { Name = name, StoreId = storeId, Value = Serialize(obj) };
|
||||
ctx.Attach(settings);
|
||||
ctx.Entry(settings).State = EntityState.Modified;
|
||||
}
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
if (settings is not null)
|
||||
{
|
||||
ctx.Entry(settings).State = EntityState.Added;
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsDeadlock(DbUpdateException ex)
|
||||
{
|
||||
return ex.InnerException is Npgsql.PostgresException postgres && postgres.SqlState == "40P01";
|
||||
|
Loading…
Reference in New Issue
Block a user