diff --git a/BTCPayServerPlugins.sln b/BTCPayServerPlugins.sln index ba4fe8b..9d72191 100644 --- a/BTCPayServerPlugins.sln +++ b/BTCPayServerPlugins.sln @@ -49,6 +49,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Tests", "submo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Tests", "BTCPayServer.Plugins.Tests\BTCPayServer.Plugins.Tests.csproj", "{C6E671F6-5417-4F2F-A5A2-0A7D307BA1F7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.DataErasure", "Plugins\BTCPayServer.Plugins.DataErasure\BTCPayServer.Plugins.DataErasure.csproj", "{034D1487-81C2-4250-A26E-162579C43C18}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -233,6 +235,14 @@ Global {C6E671F6-5417-4F2F-A5A2-0A7D307BA1F7}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU {C6E671F6-5417-4F2F-A5A2-0A7D307BA1F7}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU {C6E671F6-5417-4F2F-A5A2-0A7D307BA1F7}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU + {034D1487-81C2-4250-A26E-162579C43C18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {034D1487-81C2-4250-A26E-162579C43C18}.Debug|Any CPU.Build.0 = Debug|Any CPU + {034D1487-81C2-4250-A26E-162579C43C18}.Release|Any CPU.ActiveCfg = Release|Any CPU + {034D1487-81C2-4250-A26E-162579C43C18}.Release|Any CPU.Build.0 = Release|Any CPU + {034D1487-81C2-4250-A26E-162579C43C18}.Altcoins-Debug|Any CPU.ActiveCfg = Debug|Any CPU + {034D1487-81C2-4250-A26E-162579C43C18}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU + {034D1487-81C2-4250-A26E-162579C43C18}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU + {034D1487-81C2-4250-A26E-162579C43C18}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {B19C9F52-DC47-466D-8B5C-2D202B7B003F} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962} diff --git a/Plugins/BTCPayServer.Plugins.DataErasure/BTCPayServer.Plugins.DataErasure.csproj b/Plugins/BTCPayServer.Plugins.DataErasure/BTCPayServer.Plugins.DataErasure.csproj new file mode 100644 index 0000000..b030fe6 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.DataErasure/BTCPayServer.Plugins.DataErasure.csproj @@ -0,0 +1,41 @@ + + + + net6.0 + 10 + + + + + Data Erasure + Allows you to erase user data from invoices after a period of time. + 1.0.0 + + + + true + false + true + + + + + + StaticWebAssetsEnabled=false + false + runtime;native;build;buildTransitive;contentFiles + + + + + + + + + + + + + + + diff --git a/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureController.cs b/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureController.cs new file mode 100644 index 0000000..56ae86f --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureController.cs @@ -0,0 +1,60 @@ +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Client; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Plugins.DataErasure +{ + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Route("plugins/{storeId}/DataErasure")] + public class DataErasureController : Controller + { + private readonly DataErasureService _dataErasureService; + public DataErasureController(DataErasureService dataErasureService) + { + _dataErasureService = dataErasureService; + } + + [HttpGet("")] + public async Task Update(string storeId) + { + var vm = await _dataErasureService.Get(storeId) ?? new DataErasureSettings(); + + return View(vm); + } + + [HttpPost("")] + public async Task Update(string storeId, DataErasureSettings vm, + string command) + { + if (_dataErasureService.IsRunning) + { + TempData["ErrorMessage"] = "Data erasure is currently running and cannot be changed. Please try again later."; + } + + + if (vm.Enabled) + { + if (!ModelState.IsValid) + { + return View(vm); + } + } + + + switch (command) + { + + case "save": + await _dataErasureService.Set(storeId, vm); + TempData["SuccessMessage"] = "Data erasure settings modified"; + return RedirectToAction(nameof(Update), new {storeId}); + + default: + return View(vm); + } + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.DataErasure/DataErasurePlugin.cs b/Plugins/BTCPayServer.Plugins.DataErasure/DataErasurePlugin.cs new file mode 100644 index 0000000..f017f2d --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.DataErasure/DataErasurePlugin.cs @@ -0,0 +1,23 @@ +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Abstractions.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace BTCPayServer.Plugins.DataErasure +{ + public class DataErasurePlugin : BaseBTCPayServerPlugin + { + public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } = + { + new() { Identifier = nameof(BTCPayServer), Condition = ">=1.8.0" } + }; + public override void Execute(IServiceCollection applicationBuilder) + { + applicationBuilder.AddSingleton(); + applicationBuilder.AddHostedService( sp => sp.GetRequiredService()); + applicationBuilder.AddSingleton(new UIExtension("DataErasure/DataErasureNav", + "store-integrations-nav")); + base.Execute(applicationBuilder); + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureService.cs b/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureService.cs new file mode 100644 index 0000000..44c5ec1 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureService.cs @@ -0,0 +1,132 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Services.Invoices; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace BTCPayServer.Plugins.DataErasure +{ + public class DataErasureService : IHostedService + { + private readonly IStoreRepository _storeRepository; + private readonly ILogger _logger; + private readonly InvoiceRepository _invoiceRepository; + + public DataErasureService(IStoreRepository storeRepository, ILogger logger, + InvoiceRepository invoiceRepository) + { + _storeRepository = storeRepository; + _logger = logger; + _invoiceRepository = invoiceRepository; + } + + public async Task Get(string storeId) + { + return await _storeRepository.GetSettingAsync(storeId, + nameof(DataErasureSettings)); + } + + public async Task Set(string storeId, DataErasureSettings settings) + { + await _runningLock.WaitAsync(); + var existing = await Get(storeId); + settings.LastRunCutoff = existing?.LastRunCutoff; + await SetCore(storeId, settings); + _runningLock.Release(); + } + + private async Task SetCore(string storeId, DataErasureSettings settings) + { + await _storeRepository.UpdateSetting(storeId, nameof(DataErasureSettings), settings); + } + + public bool IsRunning { get; private set; } + private readonly SemaphoreSlim _runningLock = new(1, 1); + + private async Task Run(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await _runningLock.WaitAsync(cancellationToken); + IsRunning = true; + + + var settings = + await _storeRepository.GetSettingsAsync(nameof(DataErasureSettings)); + foreach (var setting in settings.Where(setting => setting.Value.Enabled)) + { + var skip = 0; + var count = 0; + var cutoffDate = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(setting.Value.DaysToKeep)); + while (true) + { + var invoices = await _invoiceRepository.GetInvoices(new InvoiceQuery() + { + StartDate = setting.Value.LastRunCutoff, + EndDate = cutoffDate, + StoreId = new[] {setting.Key}, + Skip = skip, + Take = 100 + }); + foreach (var invoice in invoices) + { + //replace all buyer info with "erased" + if (!string.IsNullOrEmpty(invoice.Metadata.BuyerAddress1)) + invoice.Metadata.BuyerAddress1 = "erased"; + if (!string.IsNullOrEmpty(invoice.Metadata.BuyerAddress2)) + invoice.Metadata.BuyerAddress2 = "erased"; + if (!string.IsNullOrEmpty(invoice.Metadata.BuyerCity)) + invoice.Metadata.BuyerCity = "erased"; + if (!string.IsNullOrEmpty(invoice.Metadata.BuyerCountry)) + invoice.Metadata.BuyerCountry = "erased"; + if (!string.IsNullOrEmpty(invoice.Metadata.BuyerEmail)) + invoice.Metadata.BuyerEmail = "erased"; + if (!string.IsNullOrEmpty(invoice.Metadata.BuyerName)) + invoice.Metadata.BuyerName = "erased"; + if (!string.IsNullOrEmpty(invoice.Metadata.BuyerPhone)) + invoice.Metadata.BuyerPhone = "erased"; + if (!string.IsNullOrEmpty(invoice.Metadata.BuyerState)) + invoice.Metadata.BuyerState = "erased"; + if (!string.IsNullOrEmpty(invoice.Metadata.BuyerZip)) + invoice.Metadata.BuyerZip = "erased"; + await _invoiceRepository.UpdateInvoiceMetadata(invoice.Id, invoice.StoreId, + invoice.Metadata.ToJObject()); + count++; + } + + if (invoices.Length < 100) + { + break; + } + + skip += 100; + } + + _logger.LogInformation($"Erased {count} invoice data for store {setting.Key}"); + setting.Value.LastRunCutoff = cutoffDate; + await SetCore(setting.Key, setting.Value); + } + + IsRunning = false; + + + _runningLock.Release(); + await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken); + } + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _ = Run(cancellationToken); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureSettings.cs b/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureSettings.cs new file mode 100644 index 0000000..94a2dbb --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureSettings.cs @@ -0,0 +1,11 @@ +using System; + +namespace BTCPayServer.Plugins.DataErasure +{ + public class DataErasureSettings + { + public bool Enabled { get; set; } + public int DaysToKeep { get; set; } + public DateTimeOffset? LastRunCutoff { get; set; } + } +} diff --git a/Plugins/BTCPayServer.Plugins.DataErasure/Pack.ps1 b/Plugins/BTCPayServer.Plugins.DataErasure/Pack.ps1 new file mode 100644 index 0000000..258d46b --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.DataErasure/Pack.ps1 @@ -0,0 +1,2 @@ +dotnet publish -c Release -o bin/publish/BTCPayServer.Plugins.DataErasure +dotnet run -p ../../BTCPayServer.PluginPacker bin/publish/BTCPayServer.Plugins.DataErasure BTCPayServer.Plugins.DataErasure ../packed diff --git a/Plugins/BTCPayServer.Plugins.DataErasure/Views/DataErasure/Update.cshtml b/Plugins/BTCPayServer.Plugins.DataErasure/Views/DataErasure/Update.cshtml new file mode 100644 index 0000000..313e09a --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.DataErasure/Views/DataErasure/Update.cshtml @@ -0,0 +1,38 @@ +@using BTCPayServer.Abstractions.Extensions +@using Microsoft.AspNetCore.Mvc.TagHelpers +@model BTCPayServer.Plugins.DataErasure.DataErasureSettings +@{ + ViewData.SetActivePage("DataErasure", "Data Erasure", "DataErasure"); +} + + + +

@ViewData["Title"]

+ +
+

+ This plugin erases buyer information on your store's invoices based on when they were created. THERE IS NO UNDOING THIS ACTION ONCE IT HAS EXECUTED. +

+
+
+
+
+
+
+ + +
+
+
+ + + +
+ @if (Model.LastRunCutoff != null) + { +
Cleared data up to @Model.LastRunCutoff.Value.ToString("g")
+ } + +
+
+
\ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.DataErasure/Views/Shared/DataErasure/DataErasureNav.cshtml b/Plugins/BTCPayServer.Plugins.DataErasure/Views/Shared/DataErasure/DataErasureNav.cshtml new file mode 100644 index 0000000..308a4ab --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.DataErasure/Views/Shared/DataErasure/DataErasureNav.cshtml @@ -0,0 +1,47 @@ +@using BTCPayServer.Abstractions.Contracts +@using BTCPayServer.Abstractions.Extensions +@using Microsoft.AspNetCore.Mvc.TagHelpers +@inject IScopeProvider ScopeProvider +@{ + var storeId = ScopeProvider.GetCurrentStoreId(); +} +@if (!string.IsNullOrEmpty(storeId)) +{ + +} diff --git a/Plugins/BTCPayServer.Plugins.DataErasure/Views/_ViewImports.cshtml b/Plugins/BTCPayServer.Plugins.DataErasure/Views/_ViewImports.cshtml new file mode 100644 index 0000000..cf06ff9 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.DataErasure/Views/_ViewImports.cshtml @@ -0,0 +1,9 @@ +@using BTCPayServer.Abstractions.Extensions +@inject BTCPayServer.Abstractions.Services.Safe Safe +@addTagHelper *, BTCPayServer.Abstractions +@addTagHelper *, BTCPayServer.TagHelpers +@addTagHelper *, BTCPayServer.Views.TagHelpers +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, BTCPayServer \ No newline at end of file