mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-18 05:12:51 +01:00
BTCPay Extensions Part 2 (#2001)
* BTCPay Extensions Part 2 This PR cleans up the extension system a bit in that: * It renames the test extension to a more uniform name * Allows yo uto have system extensions, which are extensions but bundled by default with the release (and cannot be removed) * Adds a tool to help you generate an extension package from a csproj * Refactors the UI extension points to a view component * Moves some more interfaces to the Abstractions csproj * Rename to plugins
This commit is contained in:
parent
362ba21567
commit
5979fe5eef
1
.gitignore
vendored
1
.gitignore
vendored
@ -298,3 +298,4 @@ BTCPayServer/wwwroot/bundles/*
|
|||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
BTCPayServer/testpwd
|
BTCPayServer/testpwd
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
Packed Plugins
|
||||||
|
6
.run/Build and pack extensions.run.xml
Normal file
6
.run/Build and pack extensions.run.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="Build and pack plugins" type="CompoundRunConfigurationType">
|
||||||
|
<toRun name="Pack Test Plugin" type="DotNetProject" />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
21
.run/Pack Test Extension.run.xml
Normal file
21
.run/Pack Test Extension.run.xml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="Pack Test Plugin" type="DotNetProject" factoryName=".NET Project" singleton="false">
|
||||||
|
<option name="EXE_PATH" value="$PROJECT_DIR$/BTCPayServer.PluginPacker/bin/Debug/netcoreapp3.1/BTCPayServer.PluginPacker.dll" />
|
||||||
|
<option name="PROGRAM_PARAMETERS" value="../../../../BTCPayServer.Plugins.Test\bin\Debug\netcoreapp3.1 BTCPayServer.Plugins.Test "../../../../Packed Plugins"" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/BTCPayServer.PluginPacker/bin/Debug/netcoreapp3.1" />
|
||||||
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="USE_MONO" value="0" />
|
||||||
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/BTCPayServer.PluginPacker/BTCPayServer.PluginPacker.csproj" />
|
||||||
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
|
<option name="PROJECT_TFM" value=".NETCoreApp,Version=v3.1" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Build" default="false" projectName="BTCPayServer.Plugins.Test" projectPath="C:\Git\btcpayserver\BTCPayServer.Plugins.Test\BTCPayServer.Plugins.Test.csproj" />
|
||||||
|
<option name="Build" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
@ -1,16 +1,20 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using BTCPayServer.Abstractions.Converters;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace BTCPayServer.Contracts
|
namespace BTCPayServer.Contracts
|
||||||
{
|
{
|
||||||
public interface IBTCPayServerExtension
|
public interface IBTCPayServerPlugin
|
||||||
{
|
{
|
||||||
public string Identifier { get;}
|
public string Identifier { get; }
|
||||||
string Name { get; }
|
string Name { get; }
|
||||||
|
[JsonConverter(typeof(VersionConverter))]
|
||||||
Version Version { get; }
|
Version Version { get; }
|
||||||
string Description { get; }
|
string Description { get; }
|
||||||
|
bool SystemPlugin { get; set; }
|
||||||
|
string[] Dependencies { get; }
|
||||||
void Execute(IApplicationBuilder applicationBuilder, IServiceProvider applicationBuilderApplicationServices);
|
void Execute(IApplicationBuilder applicationBuilder, IServiceProvider applicationBuilderApplicationServices);
|
||||||
void Execute(IServiceCollection applicationBuilder);
|
void Execute(IServiceCollection applicationBuilder);
|
||||||
}
|
}
|
12
BTCPayServer.Abstractions/Contracts/ISettingsRepository.cs
Normal file
12
BTCPayServer.Abstractions/Contracts/ISettingsRepository.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Services
|
||||||
|
{
|
||||||
|
public interface ISettingsRepository
|
||||||
|
{
|
||||||
|
Task<T> GetSettingAsync<T>(string name = null);
|
||||||
|
Task UpdateSetting<T>(T obj, string name = null);
|
||||||
|
Task<T> WaitSettingsChanged<T>(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,15 @@
|
|||||||
namespace BTCPayServer.Contracts
|
namespace BTCPayServer.Contracts
|
||||||
{
|
{
|
||||||
public interface INavExtension
|
public interface IUIExtension
|
||||||
{
|
{
|
||||||
string Partial { get; }
|
string Partial { get; }
|
||||||
|
|
||||||
string Location { get; }
|
string Location { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NavExtension: INavExtension
|
public class UIExtension: IUIExtension
|
||||||
{
|
{
|
||||||
public NavExtension(string partial, string location)
|
public UIExtension(string partial, string location)
|
||||||
{
|
{
|
||||||
Partial = partial;
|
Partial = partial;
|
||||||
Location = location;
|
Location = location;
|
||||||
|
19
BTCPayServer.Abstractions/Converters/VersionConverter.cs
Normal file
19
BTCPayServer.Abstractions/Converters/VersionConverter.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Abstractions.Converters
|
||||||
|
{
|
||||||
|
public class VersionConverter : JsonConverter<Version>
|
||||||
|
{
|
||||||
|
public override Version Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
return Version.Parse(reader.GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStringValue(value.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
BTCPayServer.Abstractions/Extensions/Extensions.cs
Normal file
20
BTCPayServer.Abstractions/Extensions/Extensions.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using BTCPayServer.Models;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||||
|
|
||||||
|
namespace BTCPayServer
|
||||||
|
{
|
||||||
|
public static class SetStatusMessageModelExtensions
|
||||||
|
{
|
||||||
|
public static void SetStatusMessageModel(this ITempDataDictionary tempData, StatusMessageModel statusMessage)
|
||||||
|
{
|
||||||
|
if (statusMessage == null)
|
||||||
|
{
|
||||||
|
tempData.Remove("StatusMessageModel");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tempData["StatusMessageModel"] = JsonSerializer.Serialize(statusMessage, new JsonSerializerOptions());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
BTCPayServer.Abstractions/Models/BaseIbtcPayServerPlugin.cs
Normal file
36
BTCPayServer.Abstractions/Models/BaseIbtcPayServerPlugin.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using BTCPayServer.Contracts;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Models
|
||||||
|
{
|
||||||
|
public abstract class BaseBTCPayServerPlugin : IBTCPayServerPlugin
|
||||||
|
{
|
||||||
|
public abstract string Identifier { get; }
|
||||||
|
public abstract string Name { get; }
|
||||||
|
|
||||||
|
public virtual Version Version
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return Assembly.GetAssembly(GetType())?.GetName().Version ?? new Version(1, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract string Description { get; }
|
||||||
|
public bool SystemPlugin { get; set; }
|
||||||
|
public bool SystemExtension { get; set; }
|
||||||
|
public virtual string[] Dependencies { get; } = Array.Empty<string>();
|
||||||
|
|
||||||
|
public virtual void Execute(IApplicationBuilder applicationBuilder,
|
||||||
|
IServiceProvider applicationBuilderApplicationServices)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Execute(IServiceCollection applicationBuilder)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -24,7 +24,7 @@ namespace BTCPayServer
|
|||||||
var settings = new BTCPayDefaultSettings();
|
var settings = new BTCPayDefaultSettings();
|
||||||
_Settings.Add(chainType, settings);
|
_Settings.Add(chainType, settings);
|
||||||
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", NBXplorerDefaultSettings.GetFolderName(chainType));
|
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", NBXplorerDefaultSettings.GetFolderName(chainType));
|
||||||
settings.DefaultExtensionDirectory =
|
settings.DefaultPluginDirectory =
|
||||||
StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", "Extensions");
|
StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", "Extensions");
|
||||||
settings.DefaultConfigurationFile = Path.Combine(settings.DefaultDataDirectory, "settings.config");
|
settings.DefaultConfigurationFile = Path.Combine(settings.DefaultDataDirectory, "settings.config");
|
||||||
settings.DefaultPort = (chainType == NetworkType.Mainnet ? 23000 :
|
settings.DefaultPort = (chainType == NetworkType.Mainnet ? 23000 :
|
||||||
@ -41,7 +41,7 @@ namespace BTCPayServer
|
|||||||
}
|
}
|
||||||
|
|
||||||
public string DefaultDataDirectory { get; set; }
|
public string DefaultDataDirectory { get; set; }
|
||||||
public string DefaultExtensionDirectory { get; set; }
|
public string DefaultPluginDirectory { get; set; }
|
||||||
public string DefaultConfigurationFile { get; set; }
|
public string DefaultConfigurationFile { get; set; }
|
||||||
public int DefaultPort { get; set; }
|
public int DefaultPort { get; set; }
|
||||||
}
|
}
|
||||||
|
13
BTCPayServer.PluginPacker/BTCPayServer.PluginPacker.csproj
Normal file
13
BTCPayServer.PluginPacker/BTCPayServer.PluginPacker.csproj
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
|
<AssemblyVersion>1.0.0.1</AssemblyVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
50
BTCPayServer.PluginPacker/Program.cs
Normal file
50
BTCPayServer.PluginPacker/Program.cs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
|
using BTCPayServer.Contracts;
|
||||||
|
|
||||||
|
namespace BTCPayServer.PluginPacker
|
||||||
|
{
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
var directory = args[0];
|
||||||
|
var name = args[1];
|
||||||
|
var outputDir = args[2];
|
||||||
|
var outputFile = Path.Combine(outputDir, name);
|
||||||
|
var rootDLLPath = Path.Combine(directory, name +".dll");
|
||||||
|
if (!File.Exists(rootDLLPath) )
|
||||||
|
{
|
||||||
|
throw new Exception($"{rootDLLPath} could not be found");
|
||||||
|
}
|
||||||
|
|
||||||
|
var assembly = Assembly.LoadFrom(rootDLLPath);
|
||||||
|
var extension = GetAllExtensionTypesFromAssembly(assembly).FirstOrDefault();
|
||||||
|
if (extension is null)
|
||||||
|
{
|
||||||
|
throw new Exception($"{rootDLLPath} is not a valid plugin");
|
||||||
|
}
|
||||||
|
|
||||||
|
var loadedPlugin = (IBTCPayServerPlugin)Activator.CreateInstance(extension);
|
||||||
|
var json = JsonSerializer.Serialize(loadedPlugin);
|
||||||
|
Directory.CreateDirectory(outputDir);
|
||||||
|
if (File.Exists(outputFile + ".btcpay"))
|
||||||
|
{
|
||||||
|
File.Delete(outputFile + ".btcpay");
|
||||||
|
}
|
||||||
|
ZipFile.CreateFromDirectory(directory, outputFile + ".btcpay", CompressionLevel.Optimal, false);
|
||||||
|
File.WriteAllText(outputFile + ".btcpay.json", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Type[] GetAllExtensionTypesFromAssembly(Assembly assembly)
|
||||||
|
{
|
||||||
|
return assembly.GetTypes().Where(type =>
|
||||||
|
typeof(IBTCPayServerPlugin).IsAssignableFrom(type) &&
|
||||||
|
!type.IsAbstract).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
BTCPayServer.PluginPacker/README.md
Normal file
4
BTCPayServer.PluginPacker/README.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
This tool makes it easy to create a BTCPay plugin package. To create a package you must:
|
||||||
|
* Build your BTCPay Plugin project
|
||||||
|
* open a terminal in this project
|
||||||
|
* run `dotnet run PATH_TO_PLUGIN_BUILD_DIRECTORY NAME_OF_PLUGIN BUILT_PACKAGE_OUTPUT_DIRECTORY`
|
@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc.Controllers;
|
|||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace BTCPayServer.Test
|
namespace BTCPayServer.Plugins.Test
|
||||||
{
|
{
|
||||||
public class ApplicationPartsLogger : IHostedService
|
public class ApplicationPartsLogger : IHostedService
|
||||||
{
|
{
|
@ -4,6 +4,7 @@
|
|||||||
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
|
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
|
||||||
<PreserveCompilationContext>false</PreserveCompilationContext>
|
<PreserveCompilationContext>false</PreserveCompilationContext>
|
||||||
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
|
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
|
||||||
|
<AssemblyVersion>1.0.0</AssemblyVersion>
|
||||||
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
19
BTCPayServer.Plugins.Test/TestExtension.cs
Normal file
19
BTCPayServer.Plugins.Test/TestExtension.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using BTCPayServer.Contracts;
|
||||||
|
using BTCPayServer.Models;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Plugins.Test
|
||||||
|
{
|
||||||
|
public class TestPlugin: BaseBTCPayServerPlugin
|
||||||
|
{
|
||||||
|
public override string Identifier { get; } = "BTCPayServer.Plugins.Test";
|
||||||
|
public override string Name { get; } = "Test Plugin!";
|
||||||
|
public override string Description { get; } = "This is a description of the loaded test extension!";
|
||||||
|
|
||||||
|
public override void Execute(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton<IUIExtension>(new UIExtension("TestExtensionNavExtension", "header-nav"));
|
||||||
|
services.AddHostedService<ApplicationPartsLogger>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace BTCPayServer.Test
|
namespace BTCPayServer.Plugins.Test
|
||||||
{
|
{
|
||||||
[Route("extensions/test")]
|
[Route("extensions/test")]
|
||||||
public class TestExtensionController : Controller
|
public class TestExtensionController : Controller
|
@ -1,25 +0,0 @@
|
|||||||
using System;
|
|
||||||
using BTCPayServer.Contracts;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Test
|
|
||||||
{
|
|
||||||
public class TestExtension: IBTCPayServerExtension
|
|
||||||
{
|
|
||||||
public string Identifier { get; } = "BTCPayServer.Test";
|
|
||||||
public string Name { get; } = "Test Plugin!";
|
|
||||||
public Version Version { get; } = new Version(1,0,0,0);
|
|
||||||
public string Description { get; } = "This is a description of the loaded test extension!";
|
|
||||||
public void Execute(IApplicationBuilder applicationBuilder, IServiceProvider applicationBuilderApplicationServices)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Execute(IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddSingleton<INavExtension>(new NavExtension("TestExtensionNavExtension", "header-nav"));
|
|
||||||
services.AddHostedService<ApplicationPartsLogger>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,160 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.IO.Compression;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using BTCPayServer.Configuration;
|
|
||||||
using BTCPayServer.Contracts;
|
|
||||||
using McMaster.NETCore.Plugins;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.FileProviders;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace BTCPayServer
|
|
||||||
{
|
|
||||||
public static class ExtensionManager
|
|
||||||
{
|
|
||||||
public const string BTCPayExtensionSuffix =".btcpay";
|
|
||||||
private static readonly List<Assembly> _pluginAssemblies = new List<Assembly>();
|
|
||||||
private static ILogger _logger;
|
|
||||||
|
|
||||||
public static IMvcBuilder AddExtensions(this IMvcBuilder mvcBuilder, IServiceCollection serviceCollection,
|
|
||||||
IConfiguration config, ILoggerFactory loggerFactory)
|
|
||||||
{
|
|
||||||
_logger = loggerFactory.CreateLogger(typeof(ExtensionManager));
|
|
||||||
var extensionsFolder = config.GetExtensionDir(DefaultConfiguration.GetNetworkType(config));
|
|
||||||
var extensions = new List<IBTCPayServerExtension>();
|
|
||||||
|
|
||||||
_logger.LogInformation($"Loading extensions from {extensionsFolder}");
|
|
||||||
Directory.CreateDirectory(extensionsFolder);
|
|
||||||
ExecuteCommands(extensionsFolder);
|
|
||||||
List<(PluginLoader, Assembly, IFileProvider)> plugins = new List<(PluginLoader, Assembly, IFileProvider)>();
|
|
||||||
foreach (var dir in Directory.GetDirectories(extensionsFolder))
|
|
||||||
{
|
|
||||||
var pluginName = Path.GetFileName(dir);
|
|
||||||
|
|
||||||
var plugin = PluginLoader.CreateFromAssemblyFile(
|
|
||||||
Path.Combine(dir, pluginName + ".dll"), // create a plugin from for the .dll file
|
|
||||||
config =>
|
|
||||||
// this ensures that the version of MVC is shared between this app and the plugin
|
|
||||||
config.PreferSharedTypes = true);
|
|
||||||
|
|
||||||
mvcBuilder.AddPluginLoader(plugin);
|
|
||||||
var pluginAssembly = plugin.LoadDefaultAssembly();
|
|
||||||
_pluginAssemblies.Add(pluginAssembly);
|
|
||||||
var fileProvider = CreateEmbeddedFileProviderForAssembly(pluginAssembly);
|
|
||||||
plugins.Add((plugin, pluginAssembly, fileProvider));
|
|
||||||
extensions.AddRange(GetAllExtensionTypesFromAssembly(pluginAssembly)
|
|
||||||
.Select(GetExtensionInstanceFromType));
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var extension in extensions)
|
|
||||||
{
|
|
||||||
_logger.LogInformation($"Adding and executing extension {extension.Identifier} - {extension.Version}");
|
|
||||||
serviceCollection.AddSingleton(extension);
|
|
||||||
extension.Execute(serviceCollection);
|
|
||||||
}
|
|
||||||
|
|
||||||
return mvcBuilder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void UseExtensions(this IApplicationBuilder applicationBuilder)
|
|
||||||
{
|
|
||||||
foreach (var extension in applicationBuilder.ApplicationServices
|
|
||||||
.GetServices<IBTCPayServerExtension>())
|
|
||||||
{
|
|
||||||
extension.Execute(applicationBuilder,
|
|
||||||
applicationBuilder.ApplicationServices);
|
|
||||||
}
|
|
||||||
|
|
||||||
var webHostEnvironment = applicationBuilder.ApplicationServices.GetService<IWebHostEnvironment>();
|
|
||||||
List<IFileProvider> providers = new List<IFileProvider>() {webHostEnvironment.WebRootFileProvider};
|
|
||||||
providers.AddRange(
|
|
||||||
_pluginAssemblies
|
|
||||||
.Select(CreateEmbeddedFileProviderForAssembly));
|
|
||||||
webHostEnvironment.WebRootFileProvider = new CompositeFileProvider(providers);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Type[] GetAllExtensionTypesFromAssembly(Assembly assembly)
|
|
||||||
{
|
|
||||||
return assembly.GetTypes().Where(type =>
|
|
||||||
typeof(IBTCPayServerExtension).IsAssignableFrom(type) &&
|
|
||||||
!type.IsAbstract).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IBTCPayServerExtension GetExtensionInstanceFromType(Type type)
|
|
||||||
{
|
|
||||||
return (IBTCPayServerExtension)Activator.CreateInstance(type, Array.Empty<object>());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IFileProvider CreateEmbeddedFileProviderForAssembly(Assembly assembly)
|
|
||||||
{
|
|
||||||
return new EmbeddedFileProvider(assembly);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ExecuteCommands(string extensionsFolder)
|
|
||||||
{
|
|
||||||
var pendingCommands = GetPendingCommands(extensionsFolder);
|
|
||||||
foreach (var command in pendingCommands)
|
|
||||||
{
|
|
||||||
ExecuteCommand(command, extensionsFolder);
|
|
||||||
}
|
|
||||||
File.Delete(Path.Combine(extensionsFolder, "commands"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ExecuteCommand((string command, string extension) command, string extensionsFolder)
|
|
||||||
{
|
|
||||||
var dirName = Path.Combine(extensionsFolder, command.extension);
|
|
||||||
switch (command.command)
|
|
||||||
{
|
|
||||||
case "delete":
|
|
||||||
if (Directory.Exists(dirName))
|
|
||||||
{
|
|
||||||
Directory.Delete(dirName, true);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "install":
|
|
||||||
var fileName = dirName + BTCPayExtensionSuffix;
|
|
||||||
if (File.Exists(fileName))
|
|
||||||
{
|
|
||||||
ZipFile.ExtractToDirectory(fileName, dirName, true);
|
|
||||||
File.Delete(fileName);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static (string command, string extension)[] GetPendingCommands(string extensionsFolder)
|
|
||||||
{
|
|
||||||
if (!File.Exists(Path.Combine(extensionsFolder, "commands")))
|
|
||||||
return Array.Empty<(string command, string extension)>();
|
|
||||||
var commands = File.ReadAllLines(Path.Combine(extensionsFolder, "commands"));
|
|
||||||
return commands.Select(s =>
|
|
||||||
{
|
|
||||||
var split = s.Split(':');
|
|
||||||
return (split[0].ToLower(CultureInfo.InvariantCulture), split[1]);
|
|
||||||
}).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void QueueCommands(string extensionsFolder, params ( string action, string extension)[] commands)
|
|
||||||
{
|
|
||||||
File.AppendAllLines(Path.Combine(extensionsFolder, "commands"),
|
|
||||||
commands.Select((tuple) => $"{tuple.action}:{tuple.extension}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void CancelCommands(string extensionDir, string extension)
|
|
||||||
{
|
|
||||||
var cmds = GetPendingCommands(extensionDir).Where(tuple =>
|
|
||||||
!tuple.extension.Equals(extension, StringComparison.InvariantCultureIgnoreCase)).ToArray();
|
|
||||||
|
|
||||||
File.Delete(Path.Combine(extensionDir, "commands"));
|
|
||||||
QueueCommands(extensionDir, cmds);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
6
BTCPayServer/Components/UIExtensionPoint/Default.cshtml
Normal file
6
BTCPayServer/Components/UIExtensionPoint/Default.cshtml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
@model IEnumerable<string>
|
||||||
|
|
||||||
|
@foreach (var partial in Model)
|
||||||
|
{
|
||||||
|
await Html.RenderPartialAsync(partial);
|
||||||
|
}
|
23
BTCPayServer/Components/UIExtensionPoint/UIExtensionPoint.cs
Normal file
23
BTCPayServer/Components/UIExtensionPoint/UIExtensionPoint.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using BTCPayServer.Contracts;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Components.UIExtensionPoint
|
||||||
|
{
|
||||||
|
public class UiExtensionPoint : ViewComponent
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<IUIExtension> _uiExtensions;
|
||||||
|
|
||||||
|
public UiExtensionPoint(IEnumerable<IUIExtension> uiExtensions)
|
||||||
|
{
|
||||||
|
_uiExtensions = uiExtensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IViewComponentResult Invoke(string location)
|
||||||
|
{
|
||||||
|
return View(_uiExtensions.Where(extension => extension.Location.Equals(location, StringComparison.InvariantCultureIgnoreCase)).Select(extension => extension.Partial));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -80,7 +80,7 @@ namespace BTCPayServer.Configuration
|
|||||||
{
|
{
|
||||||
NetworkType = DefaultConfiguration.GetNetworkType(conf);
|
NetworkType = DefaultConfiguration.GetNetworkType(conf);
|
||||||
DataDir = conf.GetDataDir(NetworkType);
|
DataDir = conf.GetDataDir(NetworkType);
|
||||||
ExtensionDir = conf.GetExtensionDir(NetworkType);
|
PluginDir = conf.GetPluginDir(NetworkType);
|
||||||
Logs.Configuration.LogInformation("Network: " + NetworkType.ToString());
|
Logs.Configuration.LogInformation("Network: " + NetworkType.ToString());
|
||||||
|
|
||||||
if (conf.GetOrDefault<bool>("launchsettings", false) && NetworkType != NetworkType.Regtest)
|
if (conf.GetOrDefault<bool>("launchsettings", false) && NetworkType != NetworkType.Regtest)
|
||||||
@ -243,7 +243,7 @@ namespace BTCPayServer.Configuration
|
|||||||
DisableRegistration = conf.GetOrDefault<bool>("disable-registration", true);
|
DisableRegistration = conf.GetOrDefault<bool>("disable-registration", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ExtensionDir { get; set; }
|
public string PluginDir { get; set; }
|
||||||
|
|
||||||
private SSHSettings ParseSSHConfiguration(IConfiguration conf)
|
private SSHSettings ParseSSHConfiguration(IConfiguration conf)
|
||||||
{
|
{
|
||||||
|
@ -68,10 +68,10 @@ namespace BTCPayServer.Configuration
|
|||||||
return configuration.GetOrDefault("datadir", defaultSettings.DefaultDataDirectory);
|
return configuration.GetOrDefault("datadir", defaultSettings.DefaultDataDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetExtensionDir(this IConfiguration configuration, NetworkType networkType)
|
public static string GetPluginDir(this IConfiguration configuration, NetworkType networkType)
|
||||||
{
|
{
|
||||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(networkType);
|
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(networkType);
|
||||||
return configuration.GetOrDefault("extensiondir", defaultSettings.DefaultExtensionDirectory);
|
return configuration.GetOrDefault("plugindir", defaultSettings.DefaultPluginDirectory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,123 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using BTCPayServer.Configuration;
|
|
||||||
using BTCPayServer.Contracts;
|
|
||||||
using BTCPayServer.Models;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers
|
|
||||||
{
|
|
||||||
public partial class ServerController
|
|
||||||
{
|
|
||||||
[HttpGet("server/extensions")]
|
|
||||||
public async Task<IActionResult> ListExtensions(
|
|
||||||
[FromServices] ExtensionService extensionService,
|
|
||||||
[FromServices] BTCPayServerOptions btcPayServerOptions,
|
|
||||||
string remote = "btcpayserver/btcpayserver-extensions")
|
|
||||||
{
|
|
||||||
IEnumerable<ExtensionService.AvailableExtension> availableExtensions;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
availableExtensions = await extensionService.GetRemoteExtensions(remote);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
|
||||||
{
|
|
||||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
|
||||||
Message = "The remote could not be reached"
|
|
||||||
});
|
|
||||||
availableExtensions = Array.Empty<ExtensionService.AvailableExtension>();
|
|
||||||
}
|
|
||||||
var res = new ListExtensionsViewModel()
|
|
||||||
{
|
|
||||||
Installed = extensionService.LoadedExtensions,
|
|
||||||
Available = availableExtensions,
|
|
||||||
Remote = remote,
|
|
||||||
Commands = extensionService.GetPendingCommands(),
|
|
||||||
CanShowRestart = btcPayServerOptions.DockerDeployment
|
|
||||||
};
|
|
||||||
return View(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ListExtensionsViewModel
|
|
||||||
{
|
|
||||||
public string Remote { get; set; }
|
|
||||||
public IEnumerable<IBTCPayServerExtension> Installed { get; set; }
|
|
||||||
public IEnumerable<ExtensionService.AvailableExtension> Available { get; set; }
|
|
||||||
public (string command, string extension)[] Commands { get; set; }
|
|
||||||
public bool CanShowRestart { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("server/extensions/uninstall")]
|
|
||||||
public IActionResult UnInstallExtension(
|
|
||||||
[FromServices] ExtensionService extensionService, string extension)
|
|
||||||
{
|
|
||||||
extensionService.UninstallExtension(extension);
|
|
||||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
|
||||||
{
|
|
||||||
Message = "Extension scheduled to be uninstalled",
|
|
||||||
Severity = StatusMessageModel.StatusSeverity.Success
|
|
||||||
});
|
|
||||||
|
|
||||||
return RedirectToAction("ListExtensions");
|
|
||||||
}
|
|
||||||
[HttpPost("server/extensions/cancel")]
|
|
||||||
public IActionResult CancelExtensionCommands(
|
|
||||||
[FromServices] ExtensionService extensionService, string extension)
|
|
||||||
{
|
|
||||||
extensionService.CancelCommands(extension);
|
|
||||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
|
||||||
{
|
|
||||||
Message = "Updated",
|
|
||||||
Severity = StatusMessageModel.StatusSeverity.Success
|
|
||||||
});
|
|
||||||
|
|
||||||
return RedirectToAction("ListExtensions");
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("server/extensions/install")]
|
|
||||||
public async Task<IActionResult> InstallExtension(
|
|
||||||
[FromServices] ExtensionService extensionService, string remote, string extension)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await extensionService.DownloadRemoteExtension(remote, extension);
|
|
||||||
extensionService.InstallExtension(extension);
|
|
||||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
|
||||||
{
|
|
||||||
Message = "Extension scheduled to be installed.",
|
|
||||||
Severity = StatusMessageModel.StatusSeverity.Success
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
|
||||||
{
|
|
||||||
Message = "The extension could not be downloaded. Try again later.", Severity = StatusMessageModel.StatusSeverity.Error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return RedirectToAction("ListExtensions");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[HttpPost("server/extensions/upload")]
|
|
||||||
public async Task<IActionResult> UploadExtension([FromServices] ExtensionService extensionService,
|
|
||||||
List<IFormFile> files)
|
|
||||||
{
|
|
||||||
foreach (var formFile in files.Where(file => file.Length > 0))
|
|
||||||
{
|
|
||||||
await extensionService.UploadExtension(formFile);
|
|
||||||
extensionService.InstallExtension(formFile.FileName.TrimEnd(ExtensionManager.BTCPayExtensionSuffix,
|
|
||||||
StringComparison.InvariantCultureIgnoreCase));
|
|
||||||
}
|
|
||||||
|
|
||||||
return RedirectToAction("ListExtensions",
|
|
||||||
new {StatusMessage = "Files uploaded, restart server to load extensions"});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
124
BTCPayServer/Controllers/ServerController.Plugins.cs
Normal file
124
BTCPayServer/Controllers/ServerController.Plugins.cs
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Configuration;
|
||||||
|
using BTCPayServer.Contracts;
|
||||||
|
using BTCPayServer.Models;
|
||||||
|
using BTCPayServer.Plugins;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Controllers
|
||||||
|
{
|
||||||
|
public partial class ServerController
|
||||||
|
{
|
||||||
|
[HttpGet("server/plugins")]
|
||||||
|
public async Task<IActionResult> ListPlugins(
|
||||||
|
[FromServices] PluginService pluginService,
|
||||||
|
[FromServices] BTCPayServerOptions btcPayServerOptions,
|
||||||
|
string remote = "btcpayserver/btcpayserver-plugins")
|
||||||
|
{
|
||||||
|
IEnumerable<PluginService.AvailablePlugin> availablePlugins;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
availablePlugins = await pluginService.GetRemotePlugins(remote);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
|
{
|
||||||
|
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||||
|
Message = "The remote could not be reached"
|
||||||
|
});
|
||||||
|
availablePlugins = Array.Empty<PluginService.AvailablePlugin>();
|
||||||
|
}
|
||||||
|
var res = new ListPluginsViewModel()
|
||||||
|
{
|
||||||
|
Installed = pluginService.LoadedPlugins,
|
||||||
|
Available = availablePlugins,
|
||||||
|
Remote = remote,
|
||||||
|
Commands = pluginService.GetPendingCommands(),
|
||||||
|
CanShowRestart = btcPayServerOptions.DockerDeployment
|
||||||
|
};
|
||||||
|
return View(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ListPluginsViewModel
|
||||||
|
{
|
||||||
|
public string Remote { get; set; }
|
||||||
|
public IEnumerable<IBTCPayServerPlugin> Installed { get; set; }
|
||||||
|
public IEnumerable<PluginService.AvailablePlugin> Available { get; set; }
|
||||||
|
public (string command, string plugin)[] Commands { get; set; }
|
||||||
|
public bool CanShowRestart { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("server/plugins/uninstall")]
|
||||||
|
public IActionResult UnInstallPlugin(
|
||||||
|
[FromServices] PluginService pluginService, string plugin)
|
||||||
|
{
|
||||||
|
pluginService.UninstallPlugin(plugin);
|
||||||
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
|
{
|
||||||
|
Message = "Plugin scheduled to be uninstalled",
|
||||||
|
Severity = StatusMessageModel.StatusSeverity.Success
|
||||||
|
});
|
||||||
|
|
||||||
|
return RedirectToAction("ListPlugins");
|
||||||
|
}
|
||||||
|
[HttpPost("server/plugins/cancel")]
|
||||||
|
public IActionResult CancelPluginCommands(
|
||||||
|
[FromServices] PluginService pluginService, string plugin)
|
||||||
|
{
|
||||||
|
pluginService.CancelCommands(plugin);
|
||||||
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
|
{
|
||||||
|
Message = "Updated",
|
||||||
|
Severity = StatusMessageModel.StatusSeverity.Success
|
||||||
|
});
|
||||||
|
|
||||||
|
return RedirectToAction("ListPlugins");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("server/plugins/install")]
|
||||||
|
public async Task<IActionResult> InstallPlugin(
|
||||||
|
[FromServices] PluginService pluginService, string remote, string plugin)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await pluginService.DownloadRemotePlugin(remote, plugin);
|
||||||
|
pluginService.InstallPlugin(plugin);
|
||||||
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
|
{
|
||||||
|
Message = "Plugin scheduled to be installed.",
|
||||||
|
Severity = StatusMessageModel.StatusSeverity.Success
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
|
{
|
||||||
|
Message = "The plugin could not be downloaded. Try again later.", Severity = StatusMessageModel.StatusSeverity.Error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction("ListPlugins");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpPost("server/plugins/upload")]
|
||||||
|
public async Task<IActionResult> UploadPlugin([FromServices] PluginService pluginService,
|
||||||
|
List<IFormFile> files)
|
||||||
|
{
|
||||||
|
foreach (var formFile in files.Where(file => file.Length > 0))
|
||||||
|
{
|
||||||
|
await pluginService.UploadPlugin(formFile);
|
||||||
|
pluginService.InstallPlugin(formFile.FileName.TrimEnd(PluginManager.BTCPayPluginSuffix,
|
||||||
|
StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction("ListPlugins",
|
||||||
|
new {StatusMessage = "Files uploaded, restart server to load plugins"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -224,15 +224,7 @@ namespace BTCPayServer
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SetStatusMessageModel(this ITempDataDictionary tempData, StatusMessageModel statusMessage)
|
|
||||||
{
|
|
||||||
if (statusMessage == null)
|
|
||||||
{
|
|
||||||
tempData.Remove("StatusMessageModel");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
tempData["StatusMessageModel"] = JObject.FromObject(statusMessage).ToString(Formatting.None);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static StatusMessageModel GetStatusMessageModel(this ITempDataDictionary tempData)
|
public static StatusMessageModel GetStatusMessageModel(this ITempDataDictionary tempData)
|
||||||
{
|
{
|
||||||
|
@ -12,6 +12,7 @@ using BTCPayServer.Payments;
|
|||||||
using BTCPayServer.Payments.Bitcoin;
|
using BTCPayServer.Payments.Bitcoin;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
using BTCPayServer.Payments.PayJoin;
|
using BTCPayServer.Payments.PayJoin;
|
||||||
|
using BTCPayServer.Plugins;
|
||||||
using BTCPayServer.Security;
|
using BTCPayServer.Security;
|
||||||
using BTCPayServer.Security.Bitpay;
|
using BTCPayServer.Security.Bitpay;
|
||||||
using BTCPayServer.Security.GreenField;
|
using BTCPayServer.Security.GreenField;
|
||||||
@ -83,6 +84,7 @@ namespace BTCPayServer.Hosting
|
|||||||
services.AddEthereumLike();
|
services.AddEthereumLike();
|
||||||
#endif
|
#endif
|
||||||
services.TryAddSingleton<SettingsRepository>();
|
services.TryAddSingleton<SettingsRepository>();
|
||||||
|
services.TryAddSingleton<ISettingsRepository>(provider => provider.GetService<SettingsRepository>());
|
||||||
services.TryAddSingleton<LabelFactory>();
|
services.TryAddSingleton<LabelFactory>();
|
||||||
services.TryAddSingleton<TorServices>();
|
services.TryAddSingleton<TorServices>();
|
||||||
services.TryAddSingleton<SocketFactory>();
|
services.TryAddSingleton<SocketFactory>();
|
||||||
@ -146,7 +148,7 @@ namespace BTCPayServer.Hosting
|
|||||||
});
|
});
|
||||||
|
|
||||||
services.TryAddSingleton<AppService>();
|
services.TryAddSingleton<AppService>();
|
||||||
services.AddSingleton<ExtensionService>();
|
services.AddSingleton<PluginService>();
|
||||||
services.TryAddTransient<Safe>();
|
services.TryAddTransient<Safe>();
|
||||||
services.TryAddSingleton<Ganss.XSS.HtmlSanitizer>(o =>
|
services.TryAddSingleton<Ganss.XSS.HtmlSanitizer>(o =>
|
||||||
{
|
{
|
||||||
|
@ -6,6 +6,7 @@ using BTCPayServer.Data;
|
|||||||
using BTCPayServer.Filters;
|
using BTCPayServer.Filters;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
using BTCPayServer.PaymentRequest;
|
using BTCPayServer.PaymentRequest;
|
||||||
|
using BTCPayServer.Plugins;
|
||||||
using BTCPayServer.Security;
|
using BTCPayServer.Security;
|
||||||
using BTCPayServer.Services.Apps;
|
using BTCPayServer.Services.Apps;
|
||||||
using BTCPayServer.Storage;
|
using BTCPayServer.Storage;
|
||||||
@ -91,7 +92,7 @@ namespace BTCPayServer.Hosting
|
|||||||
#if RAZOR_RUNTIME_COMPILE
|
#if RAZOR_RUNTIME_COMPILE
|
||||||
.AddRazorRuntimeCompilation()
|
.AddRazorRuntimeCompilation()
|
||||||
#endif
|
#endif
|
||||||
.AddExtensions(services, Configuration, LoggerFactory)
|
.AddPlugins(services, Configuration, LoggerFactory)
|
||||||
.AddControllersAsServices();
|
.AddControllersAsServices();
|
||||||
|
|
||||||
|
|
||||||
@ -178,7 +179,7 @@ namespace BTCPayServer.Hosting
|
|||||||
private static void ConfigureCore(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider prov, ILoggerFactory loggerFactory, BTCPayServerOptions options)
|
private static void ConfigureCore(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider prov, ILoggerFactory loggerFactory, BTCPayServerOptions options)
|
||||||
{
|
{
|
||||||
Logs.Configure(loggerFactory);
|
Logs.Configure(loggerFactory);
|
||||||
app.UseExtensions();
|
app.UsePlugins();
|
||||||
if (env.IsDevelopment())
|
if (env.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseDeveloperExceptionPage();
|
app.UseDeveloperExceptionPage();
|
||||||
|
184
BTCPayServer/Plugins/PluginManager.cs
Normal file
184
BTCPayServer/Plugins/PluginManager.cs
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using BTCPayServer.Configuration;
|
||||||
|
using BTCPayServer.Contracts;
|
||||||
|
using McMaster.NETCore.Plugins;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.FileProviders;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Plugins
|
||||||
|
{
|
||||||
|
public static class PluginManager
|
||||||
|
{
|
||||||
|
public const string BTCPayPluginSuffix = ".btcpay";
|
||||||
|
private static readonly List<Assembly> _pluginAssemblies = new List<Assembly>();
|
||||||
|
private static ILogger _logger;
|
||||||
|
|
||||||
|
public static IMvcBuilder AddPlugins(this IMvcBuilder mvcBuilder, IServiceCollection serviceCollection,
|
||||||
|
IConfiguration config, ILoggerFactory loggerFactory)
|
||||||
|
{
|
||||||
|
_logger = loggerFactory.CreateLogger(typeof(PluginManager));
|
||||||
|
var pluginsFolder = config.GetPluginDir(DefaultConfiguration.GetNetworkType(config));
|
||||||
|
var plugins = new List<IBTCPayServerPlugin>();
|
||||||
|
|
||||||
|
_logger.LogInformation($"Loading plugins from {pluginsFolder}");
|
||||||
|
Directory.CreateDirectory(pluginsFolder);
|
||||||
|
ExecuteCommands(pluginsFolder);
|
||||||
|
List<(PluginLoader, Assembly, IFileProvider)> loadedPlugins = new List<(PluginLoader, Assembly, IFileProvider)>();
|
||||||
|
var systemExtensions = GetDefaultLoadedPluginAssemblies();
|
||||||
|
plugins.AddRange(systemExtensions.SelectMany(assembly =>
|
||||||
|
GetAllPluginTypesFromAssembly(assembly).Select(GetPluginInstanceFromType)));
|
||||||
|
foreach (IBTCPayServerPlugin btcPayServerExtension in plugins)
|
||||||
|
{
|
||||||
|
btcPayServerExtension.SystemPlugin = true;
|
||||||
|
}
|
||||||
|
foreach (var dir in Directory.GetDirectories(pluginsFolder))
|
||||||
|
{
|
||||||
|
var pluginName = Path.GetFileName(dir);
|
||||||
|
|
||||||
|
var plugin = PluginLoader.CreateFromAssemblyFile(
|
||||||
|
Path.Combine(dir, pluginName + ".dll"), // create a plugin from for the .dll file
|
||||||
|
config =>
|
||||||
|
// this ensures that the version of MVC is shared between this app and the plugin
|
||||||
|
config.PreferSharedTypes = true);
|
||||||
|
|
||||||
|
mvcBuilder.AddPluginLoader(plugin);
|
||||||
|
var pluginAssembly = plugin.LoadDefaultAssembly();
|
||||||
|
_pluginAssemblies.Add(pluginAssembly);
|
||||||
|
var fileProvider = CreateEmbeddedFileProviderForAssembly(pluginAssembly);
|
||||||
|
loadedPlugins.Add((plugin, pluginAssembly, fileProvider));
|
||||||
|
plugins.AddRange(GetAllPluginTypesFromAssembly(pluginAssembly)
|
||||||
|
.Select(GetPluginInstanceFromType));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var plugin in plugins)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
$"Adding and executing plugin {plugin.Identifier} - {plugin.Version}");
|
||||||
|
plugin.Execute(serviceCollection);
|
||||||
|
serviceCollection.AddSingleton(plugin);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError($"Error when loading plugin {plugin.Identifier} - {plugin.Version}{Environment.NewLine}{e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mvcBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void UsePlugins(this IApplicationBuilder applicationBuilder)
|
||||||
|
{
|
||||||
|
foreach (var extension in applicationBuilder.ApplicationServices
|
||||||
|
.GetServices<IBTCPayServerPlugin>())
|
||||||
|
{
|
||||||
|
extension.Execute(applicationBuilder,
|
||||||
|
applicationBuilder.ApplicationServices);
|
||||||
|
}
|
||||||
|
|
||||||
|
var webHostEnvironment = applicationBuilder.ApplicationServices.GetService<IWebHostEnvironment>();
|
||||||
|
List<IFileProvider> providers = new List<IFileProvider>() {webHostEnvironment.WebRootFileProvider};
|
||||||
|
providers.AddRange(
|
||||||
|
_pluginAssemblies
|
||||||
|
.Select(CreateEmbeddedFileProviderForAssembly));
|
||||||
|
webHostEnvironment.WebRootFileProvider = new CompositeFileProvider(providers);
|
||||||
|
}
|
||||||
|
private static Assembly[] GetDefaultLoadedPluginAssemblies()
|
||||||
|
{
|
||||||
|
return AppDomain.CurrentDomain.GetAssemblies().Where(assembly =>
|
||||||
|
assembly?.FullName?.StartsWith("BTCPayServer.Plugins",
|
||||||
|
StringComparison.InvariantCultureIgnoreCase) is true)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Type[] GetAllPluginTypesFromAssembly(Assembly assembly)
|
||||||
|
{
|
||||||
|
return assembly.GetTypes().Where(type =>
|
||||||
|
typeof(IBTCPayServerPlugin).IsAssignableFrom(type) &&
|
||||||
|
!type.IsAbstract).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IBTCPayServerPlugin GetPluginInstanceFromType(Type type)
|
||||||
|
{
|
||||||
|
return (IBTCPayServerPlugin)Activator.CreateInstance(type, Array.Empty<object>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IFileProvider CreateEmbeddedFileProviderForAssembly(Assembly assembly)
|
||||||
|
{
|
||||||
|
return new EmbeddedFileProvider(assembly);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ExecuteCommands(string pluginsFolder)
|
||||||
|
{
|
||||||
|
var pendingCommands = GetPendingCommands(pluginsFolder);
|
||||||
|
foreach (var command in pendingCommands)
|
||||||
|
{
|
||||||
|
ExecuteCommand(command, pluginsFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Delete(Path.Combine(pluginsFolder, "commands"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ExecuteCommand((string command, string extension) command, string pluginsFolder)
|
||||||
|
{
|
||||||
|
var dirName = Path.Combine(pluginsFolder, command.extension);
|
||||||
|
switch (command.command)
|
||||||
|
{
|
||||||
|
case "delete":
|
||||||
|
if (Directory.Exists(dirName))
|
||||||
|
{
|
||||||
|
Directory.Delete(dirName, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "install":
|
||||||
|
var fileName = dirName + BTCPayPluginSuffix;
|
||||||
|
if (File.Exists(fileName))
|
||||||
|
{
|
||||||
|
ZipFile.ExtractToDirectory(fileName, dirName, true);
|
||||||
|
File.Delete(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (string command, string plugin)[] GetPendingCommands(string pluginsFolder)
|
||||||
|
{
|
||||||
|
if (!File.Exists(Path.Combine(pluginsFolder, "commands")))
|
||||||
|
return Array.Empty<(string command, string plugin)>();
|
||||||
|
var commands = File.ReadAllLines(Path.Combine(pluginsFolder, "commands"));
|
||||||
|
return commands.Select(s =>
|
||||||
|
{
|
||||||
|
var split = s.Split(':');
|
||||||
|
return (split[0].ToLower(CultureInfo.InvariantCulture), split[1]);
|
||||||
|
}).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void QueueCommands(string pluginsFolder, params ( string action, string plugin)[] commands)
|
||||||
|
{
|
||||||
|
File.AppendAllLines(Path.Combine(pluginsFolder, "commands"),
|
||||||
|
commands.Select((tuple) => $"{tuple.action}:{tuple.plugin}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CancelCommands(string pluginDir, string plugin)
|
||||||
|
{
|
||||||
|
var cmds = GetPendingCommands(pluginDir).Where(tuple =>
|
||||||
|
!tuple.plugin.Equals(plugin, StringComparison.InvariantCultureIgnoreCase)).ToArray();
|
||||||
|
|
||||||
|
File.Delete(Path.Combine(pluginDir, "commands"));
|
||||||
|
QueueCommands(pluginDir, cmds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,46 +13,46 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace BTCPayServer
|
namespace BTCPayServer.Plugins
|
||||||
{
|
{
|
||||||
public class ExtensionService
|
public class PluginService
|
||||||
{
|
{
|
||||||
private readonly BTCPayServerOptions _btcPayServerOptions;
|
private readonly BTCPayServerOptions _btcPayServerOptions;
|
||||||
private readonly HttpClient _githubClient;
|
private readonly HttpClient _githubClient;
|
||||||
|
|
||||||
public ExtensionService(IEnumerable<IBTCPayServerExtension> btcPayServerExtensions,
|
public PluginService(IEnumerable<IBTCPayServerPlugin> btcPayServerPlugins,
|
||||||
IHttpClientFactory httpClientFactory, BTCPayServerOptions btcPayServerOptions)
|
IHttpClientFactory httpClientFactory, BTCPayServerOptions btcPayServerOptions)
|
||||||
{
|
{
|
||||||
LoadedExtensions = btcPayServerExtensions;
|
LoadedPlugins = btcPayServerPlugins;
|
||||||
_githubClient = httpClientFactory.CreateClient();
|
_githubClient = httpClientFactory.CreateClient();
|
||||||
_githubClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("btcpayserver", "1"));
|
_githubClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("btcpayserver", "1"));
|
||||||
_btcPayServerOptions = btcPayServerOptions;
|
_btcPayServerOptions = btcPayServerOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<IBTCPayServerExtension> LoadedExtensions { get; }
|
public IEnumerable<IBTCPayServerPlugin> LoadedPlugins { get; }
|
||||||
|
|
||||||
public async Task<IEnumerable<AvailableExtension>> GetRemoteExtensions(string remote)
|
public async Task<IEnumerable<AvailablePlugin>> GetRemotePlugins(string remote)
|
||||||
{
|
{
|
||||||
var resp = await _githubClient
|
var resp = await _githubClient
|
||||||
.GetStringAsync(new Uri($"https://api.github.com/repos/{remote}/contents"));
|
.GetStringAsync(new Uri($"https://api.github.com/repos/{remote}/contents"));
|
||||||
var files = JsonConvert.DeserializeObject<GithubFile[]>(resp);
|
var files = JsonConvert.DeserializeObject<GithubFile[]>(resp);
|
||||||
return await Task.WhenAll(files.Where(file => file.Name.EndsWith($"{ExtensionManager.BTCPayExtensionSuffix}.json", StringComparison.InvariantCulture)).Select(async file =>
|
return await Task.WhenAll(files.Where(file => file.Name.EndsWith($"{PluginManager.BTCPayPluginSuffix}.json", StringComparison.InvariantCulture)).Select(async file =>
|
||||||
{
|
{
|
||||||
return await _githubClient.GetStringAsync(file.DownloadUrl).ContinueWith(
|
return await _githubClient.GetStringAsync(file.DownloadUrl).ContinueWith(
|
||||||
task => JsonConvert.DeserializeObject<AvailableExtension>(task.Result), TaskScheduler.Current);
|
task => JsonConvert.DeserializeObject<AvailablePlugin>(task.Result), TaskScheduler.Current);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DownloadRemoteExtension(string remote, string extension)
|
public async Task DownloadRemotePlugin(string remote, string plugin)
|
||||||
{
|
{
|
||||||
var dest = _btcPayServerOptions.ExtensionDir;
|
var dest = _btcPayServerOptions.PluginDir;
|
||||||
var resp = await _githubClient
|
var resp = await _githubClient
|
||||||
.GetStringAsync(new Uri($"https://api.github.com/repos/{remote}/contents"));
|
.GetStringAsync(new Uri($"https://api.github.com/repos/{remote}/contents"));
|
||||||
var files = JsonConvert.DeserializeObject<GithubFile[]>(resp);
|
var files = JsonConvert.DeserializeObject<GithubFile[]>(resp);
|
||||||
var ext = files.SingleOrDefault(file => file.Name == $"{extension}{ExtensionManager.BTCPayExtensionSuffix}");
|
var ext = files.SingleOrDefault(file => file.Name == $"{plugin}{PluginManager.BTCPayPluginSuffix}");
|
||||||
if (ext is null)
|
if (ext is null)
|
||||||
{
|
{
|
||||||
throw new Exception("Extension not found on remote");
|
throw new Exception("Plugin not found on remote");
|
||||||
}
|
}
|
||||||
|
|
||||||
var filedest = Path.Combine(dest, ext.Name);
|
var filedest = Path.Combine(dest, ext.Name);
|
||||||
@ -60,37 +60,40 @@ namespace BTCPayServer
|
|||||||
new WebClient().DownloadFile(new Uri(ext.DownloadUrl), filedest);
|
new WebClient().DownloadFile(new Uri(ext.DownloadUrl), filedest);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void InstallExtension(string extension)
|
public void InstallPlugin(string plugin)
|
||||||
{
|
{
|
||||||
var dest = _btcPayServerOptions.ExtensionDir;
|
var dest = _btcPayServerOptions.PluginDir;
|
||||||
UninstallExtension(extension);
|
UninstallPlugin(plugin);
|
||||||
ExtensionManager.QueueCommands(dest, ("install", extension));
|
PluginManager.QueueCommands(dest, ("install", plugin));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UploadExtension(IFormFile extension)
|
public async Task UploadPlugin(IFormFile plugin)
|
||||||
{
|
{
|
||||||
var dest = _btcPayServerOptions.ExtensionDir;
|
var dest = _btcPayServerOptions.PluginDir;
|
||||||
var filedest = Path.Combine(dest, extension.FileName);
|
var filedest = Path.Combine(dest, plugin.FileName);
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(filedest));
|
Directory.CreateDirectory(Path.GetDirectoryName(filedest));
|
||||||
if (Path.GetExtension(filedest) == ExtensionManager.BTCPayExtensionSuffix)
|
if (Path.GetExtension(filedest) == PluginManager.BTCPayPluginSuffix)
|
||||||
{
|
{
|
||||||
await using var stream = new FileStream(filedest, FileMode.Create);
|
await using var stream = new FileStream(filedest, FileMode.Create);
|
||||||
await extension.CopyToAsync(stream);
|
await plugin.CopyToAsync(stream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UninstallExtension(string extension)
|
public void UninstallPlugin(string plugin)
|
||||||
{
|
{
|
||||||
var dest = _btcPayServerOptions.ExtensionDir;
|
var dest = _btcPayServerOptions.PluginDir;
|
||||||
ExtensionManager.QueueCommands(dest, ("delete", extension));
|
PluginManager.QueueCommands(dest, ("delete", plugin));
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AvailableExtension : IBTCPayServerExtension
|
public class AvailablePlugin : IBTCPayServerPlugin
|
||||||
{
|
{
|
||||||
public string Identifier { get; set; }
|
public string Identifier { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public Version Version { get; set; }
|
public Version Version { get; set; }
|
||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
|
public bool SystemPlugin { get; set; } = false;
|
||||||
|
|
||||||
|
public string[] Dependencies { get; } = Array.Empty<string>();
|
||||||
|
|
||||||
public void Execute(IApplicationBuilder applicationBuilder,
|
public void Execute(IApplicationBuilder applicationBuilder,
|
||||||
IServiceProvider applicationBuilderApplicationServices)
|
IServiceProvider applicationBuilderApplicationServices)
|
||||||
@ -111,14 +114,14 @@ namespace BTCPayServer
|
|||||||
[JsonProperty("download_url")] public string DownloadUrl { get; set; }
|
[JsonProperty("download_url")] public string DownloadUrl { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public (string command, string extension)[] GetPendingCommands()
|
public (string command, string plugin)[] GetPendingCommands()
|
||||||
{
|
{
|
||||||
return ExtensionManager.GetPendingCommands(_btcPayServerOptions.ExtensionDir);
|
return PluginManager.GetPendingCommands(_btcPayServerOptions.PluginDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CancelCommands(string extension)
|
public void CancelCommands(string plugin)
|
||||||
{
|
{
|
||||||
ExtensionManager.CancelCommands(_btcPayServerOptions.ExtensionDir, extension);
|
PluginManager.CancelCommands(_btcPayServerOptions.PluginDir, plugin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -21,7 +21,8 @@ namespace BTCPayServer.Services.Altcoins.Ethereum
|
|||||||
serviceCollection.AddSingleton<IHostedService, EthereumService>(provider => provider.GetService<EthereumService>());
|
serviceCollection.AddSingleton<IHostedService, EthereumService>(provider => provider.GetService<EthereumService>());
|
||||||
serviceCollection.AddSingleton<EthereumLikePaymentMethodHandler>();
|
serviceCollection.AddSingleton<EthereumLikePaymentMethodHandler>();
|
||||||
serviceCollection.AddSingleton<IPaymentMethodHandler>(provider => provider.GetService<EthereumLikePaymentMethodHandler>());
|
serviceCollection.AddSingleton<IPaymentMethodHandler>(provider => provider.GetService<EthereumLikePaymentMethodHandler>());
|
||||||
serviceCollection.AddSingleton<INavExtension, EthereumNavExtension>();
|
|
||||||
|
serviceCollection.AddSingleton<IUIExtension>(new UIExtension("Ethereum/StoreNavEthereumExtension", "store-nav"));
|
||||||
serviceCollection.AddTransient<NoRedirectHttpClientHandler>();
|
serviceCollection.AddTransient<NoRedirectHttpClientHandler>();
|
||||||
serviceCollection.AddSingleton<ISyncSummaryProvider, EthereumSyncSummaryProvider>();
|
serviceCollection.AddSingleton<ISyncSummaryProvider, EthereumSyncSummaryProvider>();
|
||||||
serviceCollection.AddHttpClient(EthereumInvoiceCreateHttpClient)
|
serviceCollection.AddHttpClient(EthereumInvoiceCreateHttpClient)
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
#if ALTCOINS
|
|
||||||
using BTCPayServer.Contracts;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Altcoins.Ethereum
|
|
||||||
{
|
|
||||||
public class EthereumNavExtension: INavExtension
|
|
||||||
{
|
|
||||||
public string Partial { get; } = "Ethereum/StoreNavEthereumExtension";
|
|
||||||
public string Location { get; } = "store";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
@ -23,7 +23,7 @@ namespace BTCPayServer.Services.Altcoins.Monero
|
|||||||
serviceCollection.AddHostedService<MoneroListener>();
|
serviceCollection.AddHostedService<MoneroListener>();
|
||||||
serviceCollection.AddSingleton<MoneroLikePaymentMethodHandler>();
|
serviceCollection.AddSingleton<MoneroLikePaymentMethodHandler>();
|
||||||
serviceCollection.AddSingleton<IPaymentMethodHandler>(provider => provider.GetService<MoneroLikePaymentMethodHandler>());
|
serviceCollection.AddSingleton<IPaymentMethodHandler>(provider => provider.GetService<MoneroLikePaymentMethodHandler>());
|
||||||
serviceCollection.AddSingleton<INavExtension, MoneroNavExtension>();
|
serviceCollection.AddSingleton<IUIExtension>(new UIExtension("Monero/StoreNavMoneroExtension", "store-nav"));
|
||||||
serviceCollection.AddSingleton<ISyncSummaryProvider, MoneroSyncSummaryProvider>();
|
serviceCollection.AddSingleton<ISyncSummaryProvider, MoneroSyncSummaryProvider>();
|
||||||
|
|
||||||
return serviceCollection;
|
return serviceCollection;
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
#if ALTCOINS
|
|
||||||
using BTCPayServer.Contracts;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Altcoins.Monero
|
|
||||||
{
|
|
||||||
public class MoneroNavExtension : INavExtension
|
|
||||||
{
|
|
||||||
public string Partial { get; } = "Monero/StoreNavMoneroExtension";
|
|
||||||
public string Location { get; } = "store";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
@ -7,7 +7,7 @@ using Newtonsoft.Json;
|
|||||||
|
|
||||||
namespace BTCPayServer.Services
|
namespace BTCPayServer.Services
|
||||||
{
|
{
|
||||||
public class SettingsRepository
|
public class SettingsRepository : ISettingsRepository
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContextFactory _ContextFactory;
|
private readonly ApplicationDbContextFactory _ContextFactory;
|
||||||
private readonly EventAggregator _EventAggregator;
|
private readonly EventAggregator _EventAggregator;
|
||||||
|
@ -7,5 +7,6 @@
|
|||||||
<a id="@ManageNavPages.U2F.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.U2F)" asp-action="U2FAuthentication">U2F Authentication</a>
|
<a id="@ManageNavPages.U2F.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.U2F)" asp-action="U2FAuthentication">U2F Authentication</a>
|
||||||
<a id="@ManageNavPages.APIKeys.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.APIKeys)" asp-action="APIKeys">API Keys</a>
|
<a id="@ManageNavPages.APIKeys.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.APIKeys)" asp-action="APIKeys">API Keys</a>
|
||||||
<a id="@ManageNavPages.Notifications.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Notifications)" asp-action="NotificationSettings">Notifications</a>
|
<a id="@ManageNavPages.Notifications.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Notifications)" asp-action="NotificationSettings">Notifications</a>
|
||||||
|
<vc:ui-extension-point location="user-nav" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,152 +0,0 @@
|
|||||||
@model BTCPayServer.Controllers.ServerController.ListExtensionsViewModel
|
|
||||||
@{
|
|
||||||
ViewData.SetActivePageAndTitle(ServerNavPages.Extensions);
|
|
||||||
var installed = Model.Installed.Select(extension => extension.Identifier);
|
|
||||||
var availableAndNotInstalled = Model.Available.Where(extension => !installed.Contains(extension.Identifier));
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (Model.Commands.Any())
|
|
||||||
{
|
|
||||||
<div class="alert alert-info">
|
|
||||||
You need to restart BTCPay Server in order to update your active extensions.
|
|
||||||
@if (Model.CanShowRestart)
|
|
||||||
{
|
|
||||||
<form method="post" asp-action="Maintenance">
|
|
||||||
<button type="submit" name="command" value="restart" class="btn btn-outline-info alert-link" asp-action="Maintenance">Restart now</button>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<partial name="_StatusMessage"/>
|
|
||||||
|
|
||||||
@if (Model.Installed.Any())
|
|
||||||
{
|
|
||||||
<h4>Installed Extensions</h4>
|
|
||||||
<div class="card-columns">
|
|
||||||
|
|
||||||
@foreach (var extension in Model.Installed)
|
|
||||||
{
|
|
||||||
var matchedAvailable = Model.Available.SingleOrDefault(availableExtension => availableExtension.Identifier == extension.Identifier);
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title">
|
|
||||||
@extension.Name <span class="badge badge-secondary">@extension.Version</span>
|
|
||||||
</h3>
|
|
||||||
<p class="card-text">@extension.Description</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
@if (matchedAvailable != null)
|
|
||||||
{
|
|
||||||
<ul class="list-group list-group-flush">
|
|
||||||
<li class="list-group-item d-flex justify-content-between">
|
|
||||||
<span>Current version</span>
|
|
||||||
<span>@extension.Version</span>
|
|
||||||
</li>
|
|
||||||
<li class="list-group-item d-flex justify-content-between">
|
|
||||||
<span>Remote version</span>
|
|
||||||
<span>@matchedAvailable.Version</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div class="card-footer">
|
|
||||||
@if (Model.Commands.Any(tuple => tuple.extension.Equals(extension.Identifier, StringComparison.InvariantCultureIgnoreCase)))
|
|
||||||
{
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<div>
|
|
||||||
<div class="badge badge-info">pending action</div></div>
|
|
||||||
<form asp-action="CancelExtensionCommands" asp-route-extension="@extension.Identifier">
|
|
||||||
<button type="submit" class="btn btn-link pt-0">Cancel</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<form asp-action="UnInstallExtension" asp-route-extension="@extension.Identifier">
|
|
||||||
<button type="submit" class="btn btn-link">Uninstall</button>
|
|
||||||
</form>
|
|
||||||
@if (extension.Version < matchedAvailable.Version)
|
|
||||||
{
|
|
||||||
<form asp-action="InstallExtension" asp-route-extension="@extension.Identifier" asp-route-remote="@Model.Remote">
|
|
||||||
<button type="submit" class="btn btn-link">Update</button>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (availableAndNotInstalled.Any())
|
|
||||||
{
|
|
||||||
<h4>Available Extensions</h4>
|
|
||||||
|
|
||||||
<div class="card-columns">
|
|
||||||
@foreach (var extension in availableAndNotInstalled)
|
|
||||||
{
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title">
|
|
||||||
@extension.Name <span class="badge badge-secondary">@extension.Version</span>
|
|
||||||
</h3>
|
|
||||||
<p class="card-text">@extension.Description</p>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer py-0 text-right">
|
|
||||||
@if (Model.Commands.Any(tuple => tuple.extension.Equals(extension.Identifier, StringComparison.InvariantCultureIgnoreCase)))
|
|
||||||
{
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<div>
|
|
||||||
<div class="badge badge-info">pending action</div></div>
|
|
||||||
<form asp-action="CancelExtensionCommands" asp-route-extension="@extension.Identifier">
|
|
||||||
<button type="submit" class="btn btn-link pt-0">Cancel</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<form asp-action="InstallExtension" asp-route-extension="@extension.Identifier" asp-route-remote="@Model.Remote">
|
|
||||||
<button type="submit" class="btn btn-link">Install</button>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<button class="btn btn-link mt-4" type="button" data-toggle="collapse" data-target="#manual-upload">
|
|
||||||
Upload extension
|
|
||||||
</button>
|
|
||||||
<div class="collapse" id="manual-upload">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title">Add extension manually</h3>
|
|
||||||
<div class="alert alert-warning">This is an extremely dangerous operation. Do not upload extensions from someone that you do not trust.</div>
|
|
||||||
<form method="post" enctype="multipart/form-data" asp-action="UploadExtension">
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="file" name="files" accept=".btcpay" id="files"/>
|
|
||||||
<button class="btn btn-primary" type="submit">Upload</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@section Scripts {
|
|
||||||
<script>
|
|
||||||
$(document).ready(function () {
|
|
||||||
|
|
||||||
$('.custom-file-input').on('change',
|
|
||||||
function () {
|
|
||||||
var label = $(this).next('label');
|
|
||||||
if (document.getElementById("file").files.length > 0) {
|
|
||||||
var fileName = document.getElementById("file").files[0].name;
|
|
||||||
label.addClass("selected").html(fileName);
|
|
||||||
} else {
|
|
||||||
label.removeClass("selected").html("Choose file");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
}
|
|
199
BTCPayServer/Views/Server/ListPlugins.cshtml
Normal file
199
BTCPayServer/Views/Server/ListPlugins.cshtml
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
@model BTCPayServer.Controllers.ServerController.ListPluginsViewModel
|
||||||
|
@{
|
||||||
|
ViewData.SetActivePageAndTitle(ServerNavPages.Plugins);
|
||||||
|
var installed = Model.Installed.Select(plugin => plugin.Identifier);
|
||||||
|
var availableAndNotInstalled = Model.Available.Where(plugin => !installed.Contains(plugin.Identifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (Model.Commands.Any())
|
||||||
|
{
|
||||||
|
<div class="alert alert-info">
|
||||||
|
You need to restart BTCPay Server in order to update your active plugins.
|
||||||
|
@if (Model.CanShowRestart)
|
||||||
|
{
|
||||||
|
<form method="post" asp-action="Maintenance">
|
||||||
|
<button type="submit" name="command" value="restart" class="btn btn-outline-info alert-link" asp-action="Maintenance">Restart now</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<partial name="_StatusMessage"/>
|
||||||
|
|
||||||
|
@if (Model.Installed.Any())
|
||||||
|
{
|
||||||
|
<h4>Installed Plugins</h4>
|
||||||
|
<div class="card-columns">
|
||||||
|
|
||||||
|
@foreach (var plugin in Model.Installed)
|
||||||
|
{
|
||||||
|
var matchedAvailable = Model.Available.SingleOrDefault(availablePlugin => availablePlugin.Identifier == plugin.Identifier);
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title">
|
||||||
|
@plugin.Name <span class="badge badge-secondary">@plugin.Version</span>
|
||||||
|
</h3>
|
||||||
|
<p class="card-text">@plugin.Description</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
@if (!plugin.SystemPlugin)
|
||||||
|
{
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item d-flex justify-content-between">
|
||||||
|
<span>Current version</span>
|
||||||
|
<span>@plugin.Version</span>
|
||||||
|
</li>
|
||||||
|
@if (matchedAvailable != null)
|
||||||
|
{
|
||||||
|
<li class="list-group-item d-flex justify-content-between">
|
||||||
|
<span>Remote version</span>
|
||||||
|
<span>@matchedAvailable.Version</span>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
<div class="card-footer">
|
||||||
|
@if (Model.Commands.Any(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase)))
|
||||||
|
{
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<div class="badge badge-info">pending action</div>
|
||||||
|
</div>
|
||||||
|
<form asp-action="CancelPluginCommands" asp-route-plugin="@plugin.Identifier">
|
||||||
|
<button type="submit" class="btn btn-link pt-0">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<form asp-action="UnInstallPlugin" asp-route-plugin="@plugin.Identifier">
|
||||||
|
<button type="submit" class="btn btn-link pt-0">Uninstall</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
@if (matchedAvailable != null && plugin.Version < matchedAvailable.Version)
|
||||||
|
{
|
||||||
|
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-remote="@Model.Remote">
|
||||||
|
<button type="submit" class="btn btn-link pt-0">Update</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (plugin.SystemPlugin)
|
||||||
|
{
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<div class="badge badge-info">system plugin</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (availableAndNotInstalled.Any())
|
||||||
|
{
|
||||||
|
<h4>Available Plugins</h4>
|
||||||
|
|
||||||
|
<div class="card-columns">
|
||||||
|
@foreach (var plugin in availableAndNotInstalled)
|
||||||
|
{
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title">
|
||||||
|
@plugin.Name <span class="badge badge-secondary">@plugin.Version</span>
|
||||||
|
</h3>
|
||||||
|
<p class="card-text">@plugin.Description</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
@if (Model.Commands.Any(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase)))
|
||||||
|
{
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<div class="badge badge-info">pending action</div>
|
||||||
|
</div>
|
||||||
|
<form asp-action="CancelPluginCommands" asp-route-plugin="@plugin.Identifier">
|
||||||
|
<button type="submit" class="btn btn-link pt-0">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-remote="@Model.Remote">
|
||||||
|
<button type="submit" class="btn btn-link pt-0">Install</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<button class="btn btn-link mt-4" type="button" data-toggle="collapse" data-target="#manual-upload">
|
||||||
|
Upload plugin
|
||||||
|
</button>
|
||||||
|
<div class="collapse" id="manual-upload">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title">Add plugin manually</h3>
|
||||||
|
<div class="alert alert-warning">This is an extremely dangerous operation. Do not upload plugins from someone that you do not trust.</div>
|
||||||
|
<form method="post" enctype="multipart/form-data" asp-action="UploadPlugin">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-file">
|
||||||
|
<input type="file" class="custom-file-input" required name="files" accept=".btcpay" id="files">
|
||||||
|
<label class="custom-file-label" for="files">Choose file</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button class="btn btn-primary" type="submit">Upload</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if (Model.Commands.Any())
|
||||||
|
{
|
||||||
|
<button class="btn btn-link mt-4" type="button" data-toggle="collapse" data-target="#pending-actions">
|
||||||
|
Pending
|
||||||
|
</button>
|
||||||
|
<div class="collapse" id="pending-actions">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title">Pending actions</h3>
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
@foreach (var extComm in Model.Commands.GroupBy(tuple => tuple.plugin))
|
||||||
|
{
|
||||||
|
<li class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
@extComm.Key (@extComm.Last().command)
|
||||||
|
<form asp-action="CancelPluginCommands" asp-route-plugin="@extComm.Key">
|
||||||
|
<button type="submit" class="btn btn-link pt-0">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
|
|
||||||
|
$('.custom-file-input').on('change',
|
||||||
|
function () {
|
||||||
|
var label = $(this).next('label');
|
||||||
|
var el = $(this).get(0);
|
||||||
|
if (el.files.length > 0) {
|
||||||
|
var fileName = el.files[0].name;
|
||||||
|
label.addClass("selected").html(fileName);
|
||||||
|
} else {
|
||||||
|
label.removeClass("selected").html("Choose file");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
@ -2,6 +2,6 @@ namespace BTCPayServer.Views.Server
|
|||||||
{
|
{
|
||||||
public enum ServerNavPages
|
public enum ServerNavPages
|
||||||
{
|
{
|
||||||
Index, Users, Emails, Policies, Theme, Services, Maintenance, Logs, Files, Extensions
|
Index, Users, Emails, Policies, Theme, Services, Maintenance, Logs, Files, Plugins
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,5 +12,6 @@
|
|||||||
}
|
}
|
||||||
<a asp-controller="Server" id="Server-@ServerNavPages.Logs" class="nav-link @ViewData.IsActivePage(ServerNavPages.Logs)" asp-action="LogsView">Logs</a>
|
<a asp-controller="Server" id="Server-@ServerNavPages.Logs" class="nav-link @ViewData.IsActivePage(ServerNavPages.Logs)" asp-action="LogsView">Logs</a>
|
||||||
<a asp-controller="Server" id="Server-@ServerNavPages.Files" class="nav-link @ViewData.IsActivePage(ServerNavPages.Files)" asp-action="Files">Files</a>
|
<a asp-controller="Server" id="Server-@ServerNavPages.Files" class="nav-link @ViewData.IsActivePage(ServerNavPages.Files)" asp-action="Files">Files</a>
|
||||||
<a asp-controller="Server" id="Server-@ServerNavPages.Extensions" class="nav-link @ViewData.IsActivePage(ServerNavPages.Extensions)" asp-action="ListExtensions">Extensions (experimental)</a>
|
<a asp-controller="Server" id="Server-@ServerNavPages.Plugins" class="nav-link @ViewData.IsActivePage(ServerNavPages.Plugins)" asp-action="ListPlugins">Plugins (experimental)</a>
|
||||||
|
<vc:ui-extension-point location="server-nav" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,12 +40,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarResponsive">
|
<div class="collapse navbar-collapse" id="navbarResponsive">
|
||||||
<ul class="navbar-nav ml-auto">
|
<ul class="navbar-nav ml-auto">
|
||||||
@inject IEnumerable<BTCPayServer.Contracts.INavExtension> Extensions;
|
<vc:ui-extension-point location="header-nav" />
|
||||||
@foreach (var extension in Extensions.Where(extension => extension.Location == "header-nav"))
|
|
||||||
{
|
|
||||||
<partial name="@extension.Partial"/>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (SignInManager.IsSignedIn(User))
|
@if (SignInManager.IsSignedIn(User))
|
||||||
{
|
{
|
||||||
if (User.IsInRole(Roles.ServerAdmin))
|
if (User.IsInRole(Roles.ServerAdmin))
|
||||||
|
@ -6,11 +6,7 @@
|
|||||||
<a id="@(nameof(StoreNavPages.Users))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Users)" asp-controller="Stores" asp-action="StoreUsers" asp-route-storeId="@this.Context.GetRouteValue("storeId")">Users</a>
|
<a id="@(nameof(StoreNavPages.Users))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Users)" asp-controller="Stores" asp-action="StoreUsers" asp-route-storeId="@this.Context.GetRouteValue("storeId")">Users</a>
|
||||||
<a id="@(nameof(StoreNavPages.PayButton))" class="nav-link @ViewData.IsActivePage(StoreNavPages.PayButton)" asp-controller="Stores" asp-action="PayButton" asp-route-storeId="@this.Context.GetRouteValue("storeId")">Pay Button</a>
|
<a id="@(nameof(StoreNavPages.PayButton))" class="nav-link @ViewData.IsActivePage(StoreNavPages.PayButton)" asp-controller="Stores" asp-action="PayButton" asp-route-storeId="@this.Context.GetRouteValue("storeId")">Pay Button</a>
|
||||||
<a id="@(nameof(StoreNavPages.Integrations))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Integrations)" asp-controller="Stores" asp-action="Integrations" asp-route-storeId="@this.Context.GetRouteValue("storeId")">Integrations</a>
|
<a id="@(nameof(StoreNavPages.Integrations))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Integrations)" asp-controller="Stores" asp-action="Integrations" asp-route-storeId="@this.Context.GetRouteValue("storeId")">Integrations</a>
|
||||||
@inject IEnumerable<BTCPayServer.Contracts.INavExtension> Extensions;
|
<vc:ui-extension-point location="store-nav" />
|
||||||
@foreach (var extension in Extensions.Where(extension => extension.Location == "store"))
|
|
||||||
{
|
|
||||||
<partial name="@extension.Partial" />
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
@ -18,6 +18,10 @@ namespace BTCPayServer.Views
|
|||||||
public static string IsActivePage<T>(this ViewDataDictionary viewData, T page)
|
public static string IsActivePage<T>(this ViewDataDictionary viewData, T page)
|
||||||
where T : IConvertible
|
where T : IConvertible
|
||||||
{
|
{
|
||||||
|
if (!viewData.ContainsKey(ACTIVE_PAGE_KEY))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
var activePage = (T)viewData[ACTIVE_PAGE_KEY];
|
var activePage = (T)viewData[ACTIVE_PAGE_KEY];
|
||||||
return page.Equals(activePage) ? "active" : null;
|
return page.Equals(activePage) ? "active" : null;
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
@inject SignInManager<ApplicationUser> SignInManager
|
@inject SignInManager<ApplicationUser> SignInManager
|
||||||
@inject BTCPayNetworkProvider BtcPayNetworkProvider
|
@inject BTCPayNetworkProvider BtcPayNetworkProvider
|
||||||
@{
|
@{
|
||||||
var wallet = WalletId.Parse( this.Context.GetRouteValue("walletId").ToString());
|
var wallet = WalletId.Parse(this.Context.GetRouteValue("walletId").ToString());
|
||||||
var network = BtcPayNetworkProvider.GetNetwork<BTCPayNetwork>(wallet.CryptoCode);
|
var network = BtcPayNetworkProvider.GetNetwork<BTCPayNetwork>(wallet.CryptoCode);
|
||||||
}
|
}
|
||||||
<div class="nav flex-column nav-pills mb-4">
|
<div class="nav flex-column nav-pills mb-4">
|
||||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Transactions)" asp-action="WalletTransactions" asp-route-walletId="@this.Context.GetRouteValue("walletId")" id="WalletTransactions">Transactions</a>
|
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Transactions)" asp-action="WalletTransactions" asp-route-walletId="@this.Context.GetRouteValue("walletId")" id="WalletTransactions">Transactions</a>
|
||||||
@if (!network.ReadonlyWallet)
|
@if (!network.ReadonlyWallet)
|
||||||
{
|
{
|
||||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Send)" asp-action="WalletSend" asp-route-walletId="@this.Context.GetRouteValue("walletId")" id="WalletSend">Send</a>
|
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Send)" asp-action="WalletSend" asp-route-walletId="@this.Context.GetRouteValue("walletId")" id="WalletSend">Send</a>
|
||||||
}
|
}
|
||||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Receive)" asp-action="WalletReceive" asp-route-walletId="@this.Context.GetRouteValue("walletId")" id="WalletReceive">Receive</a>
|
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Receive)" asp-action="WalletReceive" asp-route-walletId="@this.Context.GetRouteValue("walletId")" id="WalletReceive">Receive</a>
|
||||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Rescan)" asp-action="WalletRescan" asp-route-walletId="@this.Context.GetRouteValue("walletId")" id="WalletRescan">Rescan</a>
|
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Rescan)" asp-action="WalletRescan" asp-route-walletId="@this.Context.GetRouteValue("walletId")" id="WalletRescan">Rescan</a>
|
||||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.PullPayments)" asp-action="PullPayments" asp-route-walletId="@this.Context.GetRouteValue("walletId")" id="WalletPullPayments">Pull payments</a>
|
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.PullPayments)" asp-action="PullPayments" asp-route-walletId="@this.Context.GetRouteValue("walletId")" id="WalletPullPayments">Pull payments</a>
|
||||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Payouts)" asp-action="Payouts" asp-route-walletId="@this.Context.GetRouteValue("walletId")" id="WalletPayouts">Payouts</a>
|
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Payouts)" asp-action="Payouts" asp-route-walletId="@this.Context.GetRouteValue("walletId")" id="WalletPayouts">Payouts</a>
|
||||||
@if (!network.ReadonlyWallet)
|
@if (!network.ReadonlyWallet)
|
||||||
{
|
{
|
||||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.PSBT)" asp-action="WalletPSBT" asp-route-walletId="@this.Context.GetRouteValue("walletId")">PSBT</a>
|
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.PSBT)" asp-action="WalletPSBT" asp-route-walletId="@this.Context.GetRouteValue("walletId")">PSBT</a>
|
||||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Settings)" asp-action="WalletSettings" asp-route-walletId="@this.Context.GetRouteValue("walletId")" id="WalletSettings">Settings</a>
|
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Settings)" asp-action="WalletSettings" asp-route-walletId="@this.Context.GetRouteValue("walletId")" id="WalletSettings">Settings</a>
|
||||||
}
|
}
|
||||||
</div>
|
<vc:ui-extension-point location="wallet-nav" />
|
||||||
|
</div>
|
||||||
|
@ -30,7 +30,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer.Client", "BTCP
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Abstractions", "BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj", "{A0D50BB6-FE2C-4671-8693-F7582B66178F}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Abstractions", "BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj", "{A0D50BB6-FE2C-4671-8693-F7582B66178F}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Test", "BTCPayServer.Test\BTCPayServer.Test.csproj", "{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Test", "BTCPayServer.Plugins.Test\BTCPayServer.Plugins.Test.csproj", "{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{1FC7F660-ADF1-4D55-B61A-85C6AB083C33}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.PluginPacker", "BTCPayServer.PluginPacker\BTCPayServer.PluginPacker.csproj", "{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@ -240,6 +244,30 @@ Global
|
|||||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Release|x64.Build.0 = Release|Any CPU
|
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Release|x86.ActiveCfg = Release|Any CPU
|
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Release|x86.Build.0 = Release|Any CPU
|
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Altcoins-Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Altcoins-Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Altcoins-Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Altcoins-Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Altcoins-Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Altcoins-Release|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Altcoins-Release|x64.Build.0 = Debug|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Altcoins-Release|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Altcoins-Release|x86.Build.0 = Debug|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -247,4 +275,8 @@ Global
|
|||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {203A3162-BE45-4721-937D-6804E0E1AFF8}
|
SolutionGuid = {203A3162-BE45-4721-937D-6804E0E1AFF8}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295} = {1FC7F660-ADF1-4D55-B61A-85C6AB083C33}
|
||||||
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8} = {1FC7F660-ADF1-4D55-B61A-85C6AB083C33}
|
||||||
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
Loading…
Reference in New Issue
Block a user