mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 01:43:50 +01:00
BTCPay Server Extensions (#1925)
* BTCPay Server Extensions ![demo](https://i.imgur.com/2S00aL2.gif) * cleanup * fix * Polish UI a bit,detect when docker deployment
This commit is contained in:
parent
51a072808f
commit
1440e8c55d
11
BTCPayServer.Abstractions/BTCPayServer.Abstractions.csproj
Normal file
11
BTCPayServer.Abstractions/BTCPayServer.Abstractions.csproj
Normal file
@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
|
||||
<Import Project="../Build/Common.csproj" />
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(Altcoins)' != 'true'">
|
||||
<Compile Remove="Altcoins\**\*.cs"></Compile>
|
||||
</ItemGroup>
|
||||
</Project>
|
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BTCPayServer.Contracts
|
||||
{
|
||||
public interface IBTCPayServerExtension
|
||||
{
|
||||
public string Identifier { get;}
|
||||
string Name { get; }
|
||||
Version Version { get; }
|
||||
string Description { get; }
|
||||
|
||||
void Execute(IApplicationBuilder applicationBuilder, IServiceProvider applicationBuilderApplicationServices);
|
||||
void Execute(IServiceCollection applicationBuilder);
|
||||
}
|
||||
}
|
20
BTCPayServer.Abstractions/Contracts/INotificationHandler.cs
Normal file
20
BTCPayServer.Abstractions/Contracts/INotificationHandler.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer.Contracts
|
||||
{
|
||||
public interface INotificationHandler
|
||||
{
|
||||
string NotificationType { get; }
|
||||
Type NotificationBlobType { get; }
|
||||
void FillViewModel(object notification, NotificationViewModel vm);
|
||||
}
|
||||
|
||||
public class NotificationViewModel
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public DateTimeOffset Created { get; set; }
|
||||
public string Body { get; set; }
|
||||
public string ActionLink { get; set; }
|
||||
public bool Seen { get; set; }
|
||||
}
|
||||
}
|
21
BTCPayServer.Abstractions/Contracts/IStoreNavExtension.cs
Normal file
21
BTCPayServer.Abstractions/Contracts/IStoreNavExtension.cs
Normal file
@ -0,0 +1,21 @@
|
||||
namespace BTCPayServer.Contracts
|
||||
{
|
||||
public interface INavExtension
|
||||
{
|
||||
string Partial { get; }
|
||||
|
||||
string Location { get; }
|
||||
}
|
||||
|
||||
public class NavExtension: INavExtension
|
||||
{
|
||||
public NavExtension(string partial, string location)
|
||||
{
|
||||
Partial = partial;
|
||||
Location = location;
|
||||
}
|
||||
|
||||
public string Partial { get; }
|
||||
public string Location { get; }
|
||||
}
|
||||
}
|
@ -6,5 +6,4 @@ namespace BTCPayServer.Contracts
|
||||
|
||||
string Partial { get; }
|
||||
}
|
||||
|
||||
}
|
@ -24,6 +24,8 @@ namespace BTCPayServer
|
||||
var settings = new BTCPayDefaultSettings();
|
||||
_Settings.Add(chainType, settings);
|
||||
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", NBXplorerDefaultSettings.GetFolderName(chainType));
|
||||
settings.DefaultExtensionDirectory =
|
||||
StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", "Extensions");
|
||||
settings.DefaultConfigurationFile = Path.Combine(settings.DefaultDataDirectory, "settings.config");
|
||||
settings.DefaultPort = (chainType == NetworkType.Mainnet ? 23000 :
|
||||
chainType == NetworkType.Regtest ? 23002 :
|
||||
@ -39,6 +41,7 @@ namespace BTCPayServer
|
||||
}
|
||||
|
||||
public string DefaultDataDirectory { get; set; }
|
||||
public string DefaultExtensionDirectory { get; set; }
|
||||
public string DefaultConfigurationFile { get; set; }
|
||||
public int DefaultPort { get; set; }
|
||||
}
|
||||
|
14
BTCPayServer.Test/BTCPayServer.Test.csproj
Normal file
14
BTCPayServer.Test/BTCPayServer.Test.csproj
Normal file
@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
|
||||
<PreserveCompilationContext>false</PreserveCompilationContext>
|
||||
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
|
||||
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
|
||||
<EmbeddedResource Include="Resources\**" />
|
||||
</ItemGroup>
|
||||
</Project>
|
BIN
BTCPayServer.Test/Resources/img/screengrab.png
Normal file
BIN
BTCPayServer.Test/Resources/img/screengrab.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
25
BTCPayServer.Test/TestExtension.cs
Normal file
25
BTCPayServer.Test/TestExtension.cs
Normal file
@ -0,0 +1,25 @@
|
||||
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>();
|
||||
}
|
||||
}
|
||||
}
|
16
BTCPayServer.Test/TestExtensionController.cs
Normal file
16
BTCPayServer.Test/TestExtensionController.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Test
|
||||
{
|
||||
[Route("extensions/test")]
|
||||
public class TestExtensionController : Controller
|
||||
{
|
||||
// GET
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
|
||||
<li class="nav-item"><a asp-controller="TestExtension" asp-action="Index" class="nav-link js-scroll-trigger" >Dear Nicolas Dorier</a></li>
|
9
BTCPayServer.Test/Views/TestExtension/Index.cshtml
Normal file
9
BTCPayServer.Test/Views/TestExtension/Index.cshtml
Normal file
@ -0,0 +1,9 @@
|
||||
<section>
|
||||
<div class="container">
|
||||
<h1>Challenge Completed!!</h1>
|
||||
Here is also an image loaded from the plugin<br/>
|
||||
<a href="https://twitter.com/NicolasDorier/status/1307221679014256640">
|
||||
<img src="/Resources/img/screengrab.png"/>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
1
BTCPayServer.Test/Views/_ViewImports.cshtml
Normal file
1
BTCPayServer.Test/Views/_ViewImports.cshtml
Normal file
@ -0,0 +1 @@
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
44
BTCPayServer.Test/ss.cs
Normal file
44
BTCPayServer.Test/ss.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayServer.Test
|
||||
{
|
||||
public class ApplicationPartsLogger : IHostedService
|
||||
{
|
||||
private readonly ILogger<ApplicationPartsLogger> _logger;
|
||||
private readonly ApplicationPartManager _partManager;
|
||||
|
||||
public ApplicationPartsLogger(ILogger<ApplicationPartsLogger> logger, ApplicationPartManager partManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_partManager = partManager;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Get the names of all the application parts. This is the short assembly name for AssemblyParts
|
||||
var applicationParts = _partManager.ApplicationParts.Select(x => x.Name);
|
||||
|
||||
// Create a controller feature, and populate it from the application parts
|
||||
var controllerFeature = new ControllerFeature();
|
||||
_partManager.PopulateFeature(controllerFeature);
|
||||
|
||||
// Get the names of all of the controllers
|
||||
var controllers = controllerFeature.Controllers.Select(x => x.Name);
|
||||
|
||||
// Log the application parts and controllers
|
||||
_logger.LogInformation("Found the following application parts: '{ApplicationParts}' with the following controllers: '{Controllers}'",
|
||||
string.Join(", ", applicationParts), string.Join(", ", controllers));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Required by the interface
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
}
|
160
BTCPayServer/BTCPayExtensions/ExtensionManager.cs
Normal file
160
BTCPayServer/BTCPayExtensions/ExtensionManager.cs
Normal file
@ -0,0 +1,160 @@
|
||||
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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
124
BTCPayServer/BTCPayExtensions/ExtensionService.cs
Normal file
124
BTCPayServer/BTCPayExtensions/ExtensionService.cs
Normal file
@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Contracts;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class ExtensionService
|
||||
{
|
||||
private readonly BTCPayServerOptions _btcPayServerOptions;
|
||||
private readonly HttpClient _githubClient;
|
||||
|
||||
public ExtensionService(IEnumerable<IBTCPayServerExtension> btcPayServerExtensions,
|
||||
IHttpClientFactory httpClientFactory, BTCPayServerOptions btcPayServerOptions)
|
||||
{
|
||||
LoadedExtensions = btcPayServerExtensions;
|
||||
_githubClient = httpClientFactory.CreateClient();
|
||||
_githubClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("btcpayserver", "1"));
|
||||
_btcPayServerOptions = btcPayServerOptions;
|
||||
}
|
||||
|
||||
public IEnumerable<IBTCPayServerExtension> LoadedExtensions { get; }
|
||||
|
||||
public async Task<IEnumerable<AvailableExtension>> GetRemoteExtensions(string remote)
|
||||
{
|
||||
var resp = await _githubClient
|
||||
.GetStringAsync(new Uri($"https://api.github.com/repos/{remote}/contents"));
|
||||
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 _githubClient.GetStringAsync(file.DownloadUrl).ContinueWith(
|
||||
task => JsonConvert.DeserializeObject<AvailableExtension>(task.Result), TaskScheduler.Current);
|
||||
}));
|
||||
}
|
||||
|
||||
public async Task DownloadRemoteExtension(string remote, string extension)
|
||||
{
|
||||
var dest = _btcPayServerOptions.ExtensionDir;
|
||||
var resp = await _githubClient
|
||||
.GetStringAsync(new Uri($"https://api.github.com/repos/{remote}/contents"));
|
||||
var files = JsonConvert.DeserializeObject<GithubFile[]>(resp);
|
||||
var ext = files.SingleOrDefault(file => file.Name == $"{extension}{ExtensionManager.BTCPayExtensionSuffix}");
|
||||
if (ext is null)
|
||||
{
|
||||
throw new Exception("Extension not found on remote");
|
||||
}
|
||||
|
||||
var filedest = Path.Combine(dest, ext.Name);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(filedest));
|
||||
new WebClient().DownloadFile(new Uri(ext.DownloadUrl), filedest);
|
||||
}
|
||||
|
||||
public void InstallExtension(string extension)
|
||||
{
|
||||
var dest = _btcPayServerOptions.ExtensionDir;
|
||||
UninstallExtension(extension);
|
||||
ExtensionManager.QueueCommands(dest, ("install", extension));
|
||||
}
|
||||
|
||||
public async Task UploadExtension(IFormFile extension)
|
||||
{
|
||||
var dest = _btcPayServerOptions.ExtensionDir;
|
||||
var filedest = Path.Combine(dest, extension.FileName);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(filedest));
|
||||
if (Path.GetExtension(filedest) == ExtensionManager.BTCPayExtensionSuffix)
|
||||
{
|
||||
await using var stream = new FileStream(filedest, FileMode.Create);
|
||||
await extension.CopyToAsync(stream);
|
||||
}
|
||||
}
|
||||
|
||||
public void UninstallExtension(string extension)
|
||||
{
|
||||
var dest = _btcPayServerOptions.ExtensionDir;
|
||||
ExtensionManager.QueueCommands(dest, ("delete", extension));
|
||||
}
|
||||
|
||||
public class AvailableExtension : IBTCPayServerExtension
|
||||
{
|
||||
public string Identifier { get; set; }
|
||||
public string Name { get; set; }
|
||||
public Version Version { get; set; }
|
||||
public string Description { get; set; }
|
||||
|
||||
public void Execute(IApplicationBuilder applicationBuilder,
|
||||
IServiceProvider applicationBuilderApplicationServices)
|
||||
{
|
||||
}
|
||||
|
||||
public void Execute(IServiceCollection applicationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
class GithubFile
|
||||
{
|
||||
[JsonProperty("name")] public string Name { get; set; }
|
||||
|
||||
[JsonProperty("sha")] public string Sha { get; set; }
|
||||
|
||||
[JsonProperty("download_url")] public string DownloadUrl { get; set; }
|
||||
}
|
||||
|
||||
public (string command, string extension)[] GetPendingCommands()
|
||||
{
|
||||
return ExtensionManager.GetPendingCommands(_btcPayServerOptions.ExtensionDir);
|
||||
}
|
||||
|
||||
public void CancelCommands(string extension)
|
||||
{
|
||||
ExtensionManager.CancelCommands(_btcPayServerOptions.ExtensionDir, extension);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,8 +4,11 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
|
||||
<PreserveCompilationContext>true</PreserveCompilationContext>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
|
||||
<Compile Remove="Build\**" />
|
||||
<Compile Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<Compile Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
@ -52,6 +55,7 @@
|
||||
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
|
||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="4.0.217" />
|
||||
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.3.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.9.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
@ -141,6 +145,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\BTCPayServer.Client\BTCPayServer.Client.csproj" />
|
||||
<ProjectReference Include="..\BTCPayServer.Data\BTCPayServer.Data.csproj" />
|
||||
<ProjectReference Include="..\BTCPayServer.Rating\BTCPayServer.Rating.csproj" />
|
||||
|
@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Contracts;
|
||||
using BTCPayServer.Models.NotificationViewModels;
|
||||
|
||||
namespace BTCPayServer.Components.NotificationsDropdown
|
||||
|
@ -80,6 +80,7 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
NetworkType = DefaultConfiguration.GetNetworkType(conf);
|
||||
DataDir = conf.GetDataDir(NetworkType);
|
||||
ExtensionDir = conf.GetExtensionDir(NetworkType);
|
||||
Logs.Configuration.LogInformation("Network: " + NetworkType.ToString());
|
||||
|
||||
if (conf.GetOrDefault<bool>("launchsettings", false) && NetworkType != NetworkType.Regtest)
|
||||
@ -166,6 +167,7 @@ namespace BTCPayServer.Configuration
|
||||
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
|
||||
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
|
||||
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
|
||||
DockerDeployment = conf.GetOrDefault<bool>("dockerdeployment", true);
|
||||
AllowAdminRegistration = conf.GetOrDefault<bool>("allow-admin-registration", false);
|
||||
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
|
||||
|
||||
@ -239,6 +241,8 @@ namespace BTCPayServer.Configuration
|
||||
DisableRegistration = conf.GetOrDefault<bool>("disable-registration", true);
|
||||
}
|
||||
|
||||
public string ExtensionDir { get; set; }
|
||||
|
||||
private SSHSettings ParseSSHConfiguration(IConfiguration conf)
|
||||
{
|
||||
var settings = new SSHSettings();
|
||||
@ -281,6 +285,7 @@ namespace BTCPayServer.Configuration
|
||||
public ExternalServices ExternalServices { get; set; } = new ExternalServices();
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; set; }
|
||||
public bool DockerDeployment { get; set; }
|
||||
public string PostgresConnectionString
|
||||
{
|
||||
get;
|
||||
|
@ -67,5 +67,11 @@ namespace BTCPayServer.Configuration
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(networkType);
|
||||
return configuration.GetOrDefault("datadir", defaultSettings.DefaultDataDirectory);
|
||||
}
|
||||
|
||||
public static string GetExtensionDir(this IConfiguration configuration, NetworkType networkType)
|
||||
{
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(networkType);
|
||||
return configuration.GetOrDefault("extensiondir", defaultSettings.DefaultExtensionDirectory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
namespace BTCPayServer.Contracts
|
||||
{
|
||||
public interface IStoreNavExtension
|
||||
{
|
||||
string Partial { get; }
|
||||
}
|
||||
}
|
109
BTCPayServer/Controllers/ServerController.Extensions.cs
Normal file
109
BTCPayServer/Controllers/ServerController.Extensions.cs
Normal file
@ -0,0 +1,109 @@
|
||||
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 = "kukks/btcpayserver-extensions")
|
||||
{
|
||||
var res = new ListExtensionsViewModel()
|
||||
{
|
||||
Installed = extensionService.LoadedExtensions,
|
||||
Available = await extensionService.GetRemoteExtensions(remote),
|
||||
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 = e.Message, 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"});
|
||||
}
|
||||
}
|
||||
}
|
@ -105,6 +105,12 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> Maintenance(MaintenanceViewModel vm, string command)
|
||||
{
|
||||
vm.CanUseSSH = _sshState.CanUseSSH;
|
||||
|
||||
if (!vm.CanUseSSH)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Maintenance feature requires access to SSH properly configured in BTCPayServer configuration.";
|
||||
return View(vm);
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return View(vm);
|
||||
if (command == "changedomain")
|
||||
@ -182,6 +188,13 @@ namespace BTCPayServer.Controllers
|
||||
return error;
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"The old docker images will be cleaned soon...";
|
||||
}
|
||||
else if (command == "restart")
|
||||
{
|
||||
var error = await RunSSH(vm, $"btcpay-restart.sh");
|
||||
if (error != null)
|
||||
return error;
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"BTCPay will restart momentarily.";
|
||||
}
|
||||
else
|
||||
{
|
||||
return NotFound();
|
||||
|
18
BTCPayServer/Extensions/StringExtensions.cs
Normal file
18
BTCPayServer/Extensions/StringExtensions.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public static class StringExtensions
|
||||
{
|
||||
public static string TrimEnd(this string input, string suffixToRemove,
|
||||
StringComparison comparisonType)
|
||||
{
|
||||
if (input != null && suffixToRemove != null
|
||||
&& input.EndsWith(suffixToRemove, comparisonType))
|
||||
{
|
||||
return input.Substring(0, input.Length - suffixToRemove.Length);
|
||||
}
|
||||
else return input;
|
||||
}
|
||||
}
|
||||
}
|
@ -139,6 +139,7 @@ namespace BTCPayServer.Hosting
|
||||
});
|
||||
|
||||
services.TryAddSingleton<AppService>();
|
||||
services.AddSingleton<ExtensionService>();
|
||||
services.TryAddTransient<Safe>();
|
||||
services.TryAddSingleton<Ganss.XSS.HtmlSanitizer>(o =>
|
||||
{
|
||||
|
@ -56,7 +56,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddProviderStorage();
|
||||
services.AddSession();
|
||||
services.AddSignalR();
|
||||
services.AddMvc(o =>
|
||||
var mvcBuilder= services.AddMvc(o =>
|
||||
{
|
||||
o.Filters.Add(new XFrameOptionsAttribute("DENY"));
|
||||
o.Filters.Add(new XContentTypeOptionsAttribute("nosniff"));
|
||||
@ -91,7 +91,11 @@ namespace BTCPayServer.Hosting
|
||||
#if RAZOR_RUNTIME_COMPILE
|
||||
.AddRazorRuntimeCompilation()
|
||||
#endif
|
||||
.AddExtensions(services, Configuration, LoggerFactory)
|
||||
.AddControllersAsServices();
|
||||
|
||||
|
||||
|
||||
services.TryAddScoped<ContentSecurityPolicies>();
|
||||
services.Configure<IdentityOptions>(options =>
|
||||
{
|
||||
@ -174,6 +178,7 @@ namespace BTCPayServer.Hosting
|
||||
private static void ConfigureCore(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider prov, ILoggerFactory loggerFactory, BTCPayServerOptions options)
|
||||
{
|
||||
Logs.Configure(loggerFactory);
|
||||
app.UseExtensions();
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
|
@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Contracts;
|
||||
|
||||
namespace BTCPayServer.Models.NotificationViewModels
|
||||
{
|
||||
@ -11,12 +11,5 @@ namespace BTCPayServer.Models.NotificationViewModels
|
||||
public List<NotificationViewModel> Items { get; set; }
|
||||
}
|
||||
|
||||
public class NotificationViewModel
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public DateTimeOffset Created { get; set; }
|
||||
public string Body { get; set; }
|
||||
public string ActionLink { get; set; }
|
||||
public bool Seen { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -20,7 +20,8 @@
|
||||
"BTCPAY_DEBUGLOG": "debug.log",
|
||||
"BTCPAY_TORRCFILE": "../BTCPayServer.Tests/TestData/Tor/torrc",
|
||||
"BTCPAY_SOCKSENDPOINT": "localhost:9050",
|
||||
"BTCPAY_UPDATEURL": ""
|
||||
"BTCPAY_UPDATEURL": "",
|
||||
"BTCPAY_DOCKERDEPLOYMENT": "true"
|
||||
},
|
||||
"applicationUrl": "http://127.0.0.1:14142/"
|
||||
},
|
||||
@ -51,7 +52,8 @@
|
||||
"BTCPAY_SSHPASSWORD": "opD3i2282D",
|
||||
"BTCPAY_DEBUGLOG": "debug.log",
|
||||
"BTCPAY_TORRCFILE": "../BTCPayServer.Tests/TestData/Tor/torrc",
|
||||
"BTCPAY_SOCKSENDPOINT": "localhost:9050"
|
||||
"BTCPAY_SOCKSENDPOINT": "localhost:9050",
|
||||
"BTCPAY_DOCKERDEPLOYMENT": "true"
|
||||
},
|
||||
"applicationUrl": "https://localhost:14142/"
|
||||
},
|
||||
@ -84,7 +86,8 @@
|
||||
"BTCPAY_SSHPASSWORD": "opD3i2282D",
|
||||
"BTCPAY_DEBUGLOG": "debug.log",
|
||||
"BTCPAY_TORRCFILE": "../BTCPayServer.Tests/TestData/Tor/torrc",
|
||||
"BTCPAY_SOCKSENDPOINT": "localhost:9050"
|
||||
"BTCPAY_SOCKSENDPOINT": "localhost:9050",
|
||||
"BTCPAY_DOCKERDEPLOYMENT": "true"
|
||||
},
|
||||
"applicationUrl": "https://localhost:14142/"
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum
|
||||
serviceCollection.AddSingleton<IHostedService, EthereumService>(provider => provider.GetService<EthereumService>());
|
||||
serviceCollection.AddSingleton<EthereumLikePaymentMethodHandler>();
|
||||
serviceCollection.AddSingleton<IPaymentMethodHandler>(provider => provider.GetService<EthereumLikePaymentMethodHandler>());
|
||||
serviceCollection.AddSingleton<IStoreNavExtension, EthereumStoreNavExtension>();
|
||||
serviceCollection.AddSingleton<INavExtension, EthereumNavExtension>();
|
||||
serviceCollection.AddTransient<NoRedirectHttpClientHandler>();
|
||||
serviceCollection.AddSingleton<ISyncSummaryProvider, EthereumSyncSummaryProvider>();
|
||||
serviceCollection.AddHttpClient(EthereumInvoiceCreateHttpClient)
|
||||
|
@ -3,9 +3,10 @@ using BTCPayServer.Contracts;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Ethereum
|
||||
{
|
||||
public class EthereumStoreNavExtension: IStoreNavExtension
|
||||
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.AddSingleton<MoneroLikePaymentMethodHandler>();
|
||||
serviceCollection.AddSingleton<IPaymentMethodHandler>(provider => provider.GetService<MoneroLikePaymentMethodHandler>());
|
||||
serviceCollection.AddSingleton<IStoreNavExtension, MoneroStoreNavExtension>();
|
||||
serviceCollection.AddSingleton<INavExtension, MoneroNavExtension>();
|
||||
serviceCollection.AddSingleton<ISyncSummaryProvider, MoneroSyncSummaryProvider>();
|
||||
|
||||
return serviceCollection;
|
||||
|
@ -3,9 +3,10 @@ using BTCPayServer.Contracts;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Monero
|
||||
{
|
||||
public class MoneroStoreNavExtension : IStoreNavExtension
|
||||
public class MoneroNavExtension : INavExtension
|
||||
{
|
||||
public string Partial { get; } = "Monero/StoreNavMoneroExtension";
|
||||
public string Location { get; } = "store";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Contracts;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models.NotificationViewModels;
|
||||
|
@ -1,5 +1,5 @@
|
||||
#if DEBUG
|
||||
using BTCPayServer.Models.NotificationViewModels;
|
||||
using BTCPayServer.Contracts;
|
||||
|
||||
namespace BTCPayServer.Services.Notifications.Blobs
|
||||
{
|
||||
|
@ -1,3 +1,4 @@
|
||||
using BTCPayServer.Contracts;
|
||||
using BTCPayServer.Models.NotificationViewModels;
|
||||
|
||||
namespace BTCPayServer.Services.Notifications.Blobs
|
||||
|
@ -1,4 +1,5 @@
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Contracts;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models.NotificationViewModels;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
@ -1,14 +1,10 @@
|
||||
using System;
|
||||
using BTCPayServer.Contracts;
|
||||
using BTCPayServer.Models.NotificationViewModels;
|
||||
|
||||
namespace BTCPayServer.Services.Notifications
|
||||
{
|
||||
public interface INotificationHandler
|
||||
{
|
||||
string NotificationType { get; }
|
||||
Type NotificationBlobType { get; }
|
||||
void FillViewModel(object notification, NotificationViewModel vm);
|
||||
}
|
||||
|
||||
public abstract class NotificationHandler<TNotification> : INotificationHandler
|
||||
{
|
||||
public abstract string NotificationType { get; }
|
||||
|
@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Components.NotificationsDropdown;
|
||||
using BTCPayServer.Contracts;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.NotificationViewModels;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
153
BTCPayServer/Views/Server/ListExtensions.cshtml
Normal file
153
BTCPayServer/Views/Server/ListExtensions.cshtml
Normal file
@ -0,0 +1,153 @@
|
||||
@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>
|
||||
}
|
@ -33,6 +33,16 @@
|
||||
<button name="command" type="submit" class="btn btn-primary" value="update" disabled="@(Model.CanUseSSH ? null : "disabled")">Update</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5>Restart</h5>
|
||||
<p class="text-secondary mb-2">Restart BTCPay server and related services.</p>
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<button name="command" type="submit" class="btn btn-primary" value="restart" disabled="@(Model.CanUseSSH ? null : "disabled")">Restart</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<h5 class="mt-5">Clean</h5>
|
||||
<p class="text-secondary mb-2">Delete unused docker images present on your system.</p>
|
||||
|
@ -2,6 +2,6 @@ namespace BTCPayServer.Views.Server
|
||||
{
|
||||
public enum ServerNavPages
|
||||
{
|
||||
Index, Users, Emails, Policies, Theme, Services, Maintenance, Logs, Files
|
||||
Index, Users, Emails, Policies, Theme, Services, Maintenance, Logs, Files, Extensions
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,16 @@
|
||||
<div class="nav flex-column nav-pills mb-4">
|
||||
@using BTCPayServer.Configuration
|
||||
@inject BTCPayServerOptions BTCPayServerOptions
|
||||
<div class="nav flex-column nav-pills mb-4">
|
||||
<a asp-controller="Server" id="Server-@ServerNavPages.Users" class="nav-link @ViewData.IsActivePage(ServerNavPages.Users)" asp-action="ListUsers">Users</a>
|
||||
<a asp-controller="Server" id="Server-@ServerNavPages.Emails" class="nav-link @ViewData.IsActivePage(ServerNavPages.Emails)" asp-action="Emails">Email server</a>
|
||||
<a asp-controller="Server" id="Server-@ServerNavPages.Policies" class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</a>
|
||||
<a asp-controller="Server" id="Server-@ServerNavPages.Services" class="nav-link @ViewData.IsActivePage(ServerNavPages.Services)" asp-action="Services">Services</a>
|
||||
<a asp-controller="Server" id="Server-@ServerNavPages.Theme" class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
|
||||
<a asp-controller="Server" id="Server-@ServerNavPages.Maintenance" class="nav-link @ViewData.IsActivePage(ServerNavPages.Maintenance)" asp-action="Maintenance">Maintenance</a>
|
||||
@if (BTCPayServerOptions.DockerDeployment)
|
||||
{
|
||||
<a asp-controller="Server" id="Server-@ServerNavPages.Maintenance" class="nav-link @ViewData.IsActivePage(ServerNavPages.Maintenance)" asp-action="Maintenance">Maintenance</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.Extensions" class="nav-link @ViewData.IsActivePage(ServerNavPages.Extensions)" asp-action="ListExtensions">Extensions (experimental)</a>
|
||||
</div>
|
||||
|
@ -40,6 +40,12 @@
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarResponsive">
|
||||
<ul class="navbar-nav ml-auto">
|
||||
@inject IEnumerable<BTCPayServer.Contracts.INavExtension> Extensions;
|
||||
@foreach (var extension in Extensions.Where(extension => extension.Location == "header-nav"))
|
||||
{
|
||||
<partial name="@extension.Partial"/>
|
||||
}
|
||||
|
||||
@if (SignInManager.IsSignedIn(User))
|
||||
{
|
||||
if (User.IsInRole(Roles.ServerAdmin))
|
||||
|
@ -6,8 +6,8 @@
|
||||
<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.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.IStoreNavExtension> Extensions;
|
||||
@foreach (var extension in Extensions)
|
||||
@inject IEnumerable<BTCPayServer.Contracts.INavExtension> Extensions;
|
||||
@foreach (var extension in Extensions.Where(extension => extension.Location == "store"))
|
||||
{
|
||||
<partial name="@extension.Partial" />
|
||||
}
|
||||
|
@ -28,6 +28,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer.Data", "BTCPay
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer.Client", "BTCPayServer.Client\BTCPayServer.Client.csproj", "{21A13304-7168-49A0-86C2-0A1A9453E9C7}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Abstractions", "BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj", "{A0D50BB6-FE2C-4671-8693-F7582B66178F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Test", "BTCPayServer.Test\BTCPayServer.Test.csproj", "{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Altcoins-Debug|Any CPU = Altcoins-Debug|Any CPU
|
||||
@ -188,6 +192,54 @@ Global
|
||||
{21A13304-7168-49A0-86C2-0A1A9453E9C7}.Release|x64.Build.0 = Release|Any CPU
|
||||
{21A13304-7168-49A0-86C2-0A1A9453E9C7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{21A13304-7168-49A0-86C2-0A1A9453E9C7}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Altcoins-Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Altcoins-Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Altcoins-Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Altcoins-Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Altcoins-Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Altcoins-Release|x64.ActiveCfg = Debug|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Altcoins-Release|x64.Build.0 = Debug|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Altcoins-Release|x86.ActiveCfg = Debug|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Altcoins-Release|x86.Build.0 = Debug|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A0D50BB6-FE2C-4671-8693-F7582B66178F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Altcoins-Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Altcoins-Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Altcoins-Debug|x64.Build.0 = Debug|Any CPU
|
||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Altcoins-Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Altcoins-Debug|x86.Build.0 = Debug|Any CPU
|
||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU
|
||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Altcoins-Release|x64.ActiveCfg = Debug|Any CPU
|
||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Altcoins-Release|x64.Build.0 = Debug|Any CPU
|
||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Altcoins-Release|x86.ActiveCfg = Debug|Any CPU
|
||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Altcoins-Release|x86.Build.0 = Debug|Any CPU
|
||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295}.Release|x64.ActiveCfg = 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.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
Loading…
Reference in New Issue
Block a user