mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-03 17:36:59 +01:00
Expose LND gRPC settings
This commit is contained in:
parent
71f6aaabbd
commit
022b4f115d
11 changed files with 378 additions and 5 deletions
|
@ -114,6 +114,15 @@
|
||||||
<Folder Include="wwwroot\vendor\highlightjs\" />
|
<Folder Include="wwwroot\vendor\highlightjs\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Update="Views\Server\LNDGRPCServices.cshtml">
|
||||||
|
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||||
|
</Content>
|
||||||
|
<Content Update="Views\Server\Services.cshtml">
|
||||||
|
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="devtest.pfx">
|
<None Update="devtest.pfx">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.Configuration;
|
||||||
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Models;
|
using BTCPayServer.Models;
|
||||||
using BTCPayServer.Models.ServerViewModels;
|
using BTCPayServer.Models.ServerViewModels;
|
||||||
|
using BTCPayServer.Payments.Lightning;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Mails;
|
using BTCPayServer.Services.Mails;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
|
@ -9,6 +11,7 @@ using BTCPayServer.Validations;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using NBitcoin.DataEncoders;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
@ -27,16 +30,22 @@ namespace BTCPayServer.Controllers
|
||||||
SettingsRepository _SettingsRepository;
|
SettingsRepository _SettingsRepository;
|
||||||
private BTCPayRateProviderFactory _RateProviderFactory;
|
private BTCPayRateProviderFactory _RateProviderFactory;
|
||||||
private StoreRepository _StoreRepository;
|
private StoreRepository _StoreRepository;
|
||||||
|
LightningConfigurationProvider _LnConfigProvider;
|
||||||
|
BTCPayServerOptions _Options;
|
||||||
|
|
||||||
public ServerController(UserManager<ApplicationUser> userManager,
|
public ServerController(UserManager<ApplicationUser> userManager,
|
||||||
|
Configuration.BTCPayServerOptions options,
|
||||||
BTCPayRateProviderFactory rateProviderFactory,
|
BTCPayRateProviderFactory rateProviderFactory,
|
||||||
SettingsRepository settingsRepository,
|
SettingsRepository settingsRepository,
|
||||||
|
LightningConfigurationProvider lnConfigProvider,
|
||||||
Services.Stores.StoreRepository storeRepository)
|
Services.Stores.StoreRepository storeRepository)
|
||||||
{
|
{
|
||||||
|
_Options = options;
|
||||||
_UserManager = userManager;
|
_UserManager = userManager;
|
||||||
_SettingsRepository = settingsRepository;
|
_SettingsRepository = settingsRepository;
|
||||||
_RateProviderFactory = rateProviderFactory;
|
_RateProviderFactory = rateProviderFactory;
|
||||||
_StoreRepository = storeRepository;
|
_StoreRepository = storeRepository;
|
||||||
|
_LnConfigProvider = lnConfigProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route("server/rates")]
|
[Route("server/rates")]
|
||||||
|
@ -225,6 +234,121 @@ namespace BTCPayServer.Controllers
|
||||||
return View(settings);
|
return View(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Route("server/services")]
|
||||||
|
public IActionResult Services()
|
||||||
|
{
|
||||||
|
var result = new ServicesViewModel();
|
||||||
|
foreach (var internalNode in _Options.InternalLightningByCryptoCode)
|
||||||
|
{
|
||||||
|
//Only BTC can be supported because gRPC does not allow http path rewriting.
|
||||||
|
if (internalNode.Key == "BTC" && GetExternalLNDConnectionString(internalNode.Value) != null)
|
||||||
|
{
|
||||||
|
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
|
||||||
|
{
|
||||||
|
Crypto = internalNode.Key,
|
||||||
|
Type = "gRPC"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return View(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LightningConnectionString GetExternalLNDConnectionString(LightningConnectionString value)
|
||||||
|
{
|
||||||
|
if (value.ConnectionType != LightningConnectionType.LndREST)
|
||||||
|
return null;
|
||||||
|
var external = new LightningConnectionString();
|
||||||
|
external.ConnectionType = LightningConnectionType.LndREST;
|
||||||
|
external.BaseUri = _Options.ExternalUrl ?? value.BaseUri;
|
||||||
|
if (external.BaseUri.Scheme == "http" || value.AllowInsecure)
|
||||||
|
{
|
||||||
|
external.AllowInsecure = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (value.MacaroonFilePath != null)
|
||||||
|
external.Macaroon = System.IO.File.ReadAllBytes(value.MacaroonFilePath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (value.Macaroon != null)
|
||||||
|
external.Macaroon = value.Macaroon;
|
||||||
|
|
||||||
|
// If external url is provided, then we don't care about cert thumbprint
|
||||||
|
// because we override it at the reverse proxy level with a trusted certificate
|
||||||
|
if (_Options.ExternalUrl == null)
|
||||||
|
{
|
||||||
|
if (value.CertificateThumbprint != null)
|
||||||
|
{
|
||||||
|
external.CertificateThumbprint = value.CertificateThumbprint;
|
||||||
|
external.AllowInsecure = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return external;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("server/services/lnd-grpc/{cryptoCode}")]
|
||||||
|
public IActionResult LNDGRPCServices(string cryptoCode, ulong? secret)
|
||||||
|
{
|
||||||
|
if (!_Options.InternalLightningByCryptoCode.TryGetValue(cryptoCode.ToUpperInvariant(), out var connectionString))
|
||||||
|
return NotFound();
|
||||||
|
var model = new LNDGRPCServicesViewModel();
|
||||||
|
var external = GetExternalLNDConnectionString(connectionString);
|
||||||
|
|
||||||
|
model.Host = external.BaseUri.DnsSafeHost;
|
||||||
|
model.Port = external.BaseUri.Port;
|
||||||
|
model.SSL = external.BaseUri.Scheme == "https";
|
||||||
|
if (external.CertificateThumbprint != null)
|
||||||
|
{
|
||||||
|
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||||
|
}
|
||||||
|
if (external.Macaroon != null)
|
||||||
|
{
|
||||||
|
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
|
||||||
|
}
|
||||||
|
if (secret != null)
|
||||||
|
{
|
||||||
|
var lnConfig = _LnConfigProvider.GetConfig(secret.Value);
|
||||||
|
if (lnConfig != null)
|
||||||
|
{
|
||||||
|
model.QRCode = $"config={this.Request.GetAbsoluteRoot().WithTrailingSlash()}lnd-config/{secret.Value}/lnd.config";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
[Route("lnd-config/{secret}/lnd.config")]
|
||||||
|
public IActionResult GetLNDConfig(ulong secret)
|
||||||
|
{
|
||||||
|
var conf = _LnConfigProvider.GetConfig(secret);
|
||||||
|
if (conf == null)
|
||||||
|
return NotFound();
|
||||||
|
return Json(conf);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("server/services/lnd-grpc/{cryptoCode}")]
|
||||||
|
[HttpPost]
|
||||||
|
public IActionResult LNDGRPCServicesPOST(string cryptoCode)
|
||||||
|
{
|
||||||
|
if (!_Options.InternalLightningByCryptoCode.TryGetValue(cryptoCode.ToUpperInvariant(), out var connectionString))
|
||||||
|
return NotFound();
|
||||||
|
var external = GetExternalLNDConnectionString(connectionString);
|
||||||
|
LightningConfigurations confs = new LightningConfigurations();
|
||||||
|
LightningConfiguration conf = new LightningConfiguration();
|
||||||
|
conf.Type = "grpc";
|
||||||
|
conf.CryptoCode = cryptoCode;
|
||||||
|
conf.Host = external.BaseUri.DnsSafeHost;
|
||||||
|
conf.Port = external.BaseUri.Port;
|
||||||
|
conf.SSL = external.BaseUri.Scheme == "https";
|
||||||
|
conf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon);
|
||||||
|
conf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||||
|
confs.Configurations.Add(conf);
|
||||||
|
var secret = _LnConfigProvider.KeepConfig(confs);
|
||||||
|
return RedirectToAction(nameof(LNDGRPCServices), new { cryptoCode = cryptoCode, secret = secret });
|
||||||
|
}
|
||||||
|
|
||||||
[Route("server/theme")]
|
[Route("server/theme")]
|
||||||
public async Task<IActionResult> Theme()
|
public async Task<IActionResult> Theme()
|
||||||
{
|
{
|
||||||
|
|
|
@ -92,6 +92,7 @@ namespace BTCPayServer.Hosting
|
||||||
return opts.NetworkProvider;
|
return opts.NetworkProvider;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
services.TryAddSingleton<LightningConfigurationProvider>();
|
||||||
services.TryAddSingleton<LanguageService>();
|
services.TryAddSingleton<LanguageService>();
|
||||||
services.TryAddSingleton<NBXplorerDashboard>();
|
services.TryAddSingleton<NBXplorerDashboard>();
|
||||||
services.TryAddSingleton<StoreRepository>();
|
services.TryAddSingleton<StoreRepository>();
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Models.ServerViewModels
|
||||||
|
{
|
||||||
|
public class LNDGRPCServicesViewModel
|
||||||
|
{
|
||||||
|
public string Host { get; set; }
|
||||||
|
public int Port { get; set; }
|
||||||
|
public bool SSL { get; set; }
|
||||||
|
public string Macaroon { get; set; }
|
||||||
|
public string CertificateThumbprint { get; set; }
|
||||||
|
public string QRCode { get; set; }
|
||||||
|
}
|
||||||
|
}
|
17
BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs
Normal file
17
BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Models.ServerViewModels
|
||||||
|
{
|
||||||
|
public class ServicesViewModel
|
||||||
|
{
|
||||||
|
public class LNDServiceViewModel
|
||||||
|
{
|
||||||
|
public string Crypto { get; set; }
|
||||||
|
public string Type { get; set; }
|
||||||
|
}
|
||||||
|
public List<LNDServiceViewModel> LNDServices { get; set; } = new List<LNDServiceViewModel>();
|
||||||
|
}
|
||||||
|
}
|
|
@ -353,7 +353,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||||
public LightningConnectionType ConnectionType
|
public LightningConnectionType ConnectionType
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
private set;
|
set;
|
||||||
}
|
}
|
||||||
public byte[] Macaroon { get; set; }
|
public byte[] Macaroon { get; set; }
|
||||||
public string MacaroonFilePath { get; set; }
|
public string MacaroonFilePath { get; set; }
|
||||||
|
|
55
BTCPayServer/Services/LightningConfigurationProvider.cs
Normal file
55
BTCPayServer/Services/LightningConfigurationProvider.cs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NBitcoin;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Services
|
||||||
|
{
|
||||||
|
public class LightningConfigurationProvider
|
||||||
|
{
|
||||||
|
ConcurrentDictionary<ulong, (DateTimeOffset expiration, LightningConfigurations config)> _Map = new ConcurrentDictionary<ulong, (DateTimeOffset expiration, LightningConfigurations config)>();
|
||||||
|
public ulong KeepConfig(LightningConfigurations configuration)
|
||||||
|
{
|
||||||
|
CleanExpired();
|
||||||
|
var secret = RandomUtils.GetUInt64();
|
||||||
|
_Map.AddOrReplace(secret, (DateTimeOffset.UtcNow + TimeSpan.FromMinutes(10), configuration));
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LightningConfigurations GetConfig(ulong secret)
|
||||||
|
{
|
||||||
|
CleanExpired();
|
||||||
|
if (!_Map.TryGetValue(secret, out var value))
|
||||||
|
return null;
|
||||||
|
return value.config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanExpired()
|
||||||
|
{
|
||||||
|
foreach(var item in _Map)
|
||||||
|
{
|
||||||
|
if(item.Value.expiration < DateTimeOffset.UtcNow)
|
||||||
|
{
|
||||||
|
_Map.TryRemove(item.Key, out var unused);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LightningConfigurations
|
||||||
|
{
|
||||||
|
public List<LightningConfiguration> Configurations { get; set; } = new List<LightningConfiguration>();
|
||||||
|
}
|
||||||
|
public class LightningConfiguration
|
||||||
|
{
|
||||||
|
public string Type { get; set; }
|
||||||
|
public string CryptoCode { get; set; }
|
||||||
|
public string Host { get; set; }
|
||||||
|
public int Port { get; set; }
|
||||||
|
public bool SSL { get; set; }
|
||||||
|
public string CertificateThumbprint { get; set; }
|
||||||
|
public string Macaroon { get; set; }
|
||||||
|
}
|
||||||
|
}
|
96
BTCPayServer/Views/Server/LNDGRPCServices.cshtml
Normal file
96
BTCPayServer/Views/Server/LNDGRPCServices.cshtml
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
@model BTCPayServer.Models.ServerViewModels.LNDGRPCServicesViewModel
|
||||||
|
@{
|
||||||
|
ViewData.SetActivePageAndTitle(ServerNavPages.Services);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
<h4>GRPC settings</h4>
|
||||||
|
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div asp-validation-summary="All" class="text-danger"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="form-group">
|
||||||
|
<p>
|
||||||
|
<span>BTCPay exposes gRPC services for outside consumption, you will find connection informaiton here.<br /></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h5>QR Code connection</h5>
|
||||||
|
<p>
|
||||||
|
<span>You can use this QR Code to connect your Zap wallet to your LND instance.<br /></span>
|
||||||
|
<span>This QR Code is only valid for 10 minutes</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
@if(Model.QRCode == null)
|
||||||
|
{
|
||||||
|
<form method="post">
|
||||||
|
<button type="submit" class="btn btn-primary">Show QR Code</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div id="qrCode"></div>
|
||||||
|
<div id="qrCodeData" data-url="@Html.Raw(Model.QRCode)"></div>
|
||||||
|
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h5>More details...</h5>
|
||||||
|
<p>Alternatively, you can see the settings by clicking <a href="#details" data-toggle="collapse">here</a></p>
|
||||||
|
</div>
|
||||||
|
<div id="details" class="collapse">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Host"></label>
|
||||||
|
<input asp-for="Host" readonly class="form-control" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Port"></label>
|
||||||
|
<input asp-for="Port" readonly class="form-control" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="SSL"></label>
|
||||||
|
<input asp-for="SSL" disabled type="checkbox" class="form-check-inline" />
|
||||||
|
</div>
|
||||||
|
@if(Model.Macaroon != null)
|
||||||
|
{
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Macaroon"></label>
|
||||||
|
<input asp-for="Macaroon" readonly class="form-control" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if(Model.CertificateThumbprint != null)
|
||||||
|
{
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="CertificateThumbprint"></label>
|
||||||
|
<input asp-for="CertificateThumbprint" readonly class="form-control" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||||
|
|
||||||
|
@if(Model.QRCode != null)
|
||||||
|
{
|
||||||
|
<script type="text/javascript" src="~/js/qrcode.min.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
new QRCode(document.getElementById("qrCode"),
|
||||||
|
{
|
||||||
|
text: "@Html.Raw(Model.QRCode)",
|
||||||
|
width: 150,
|
||||||
|
height: 150
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,6 @@ namespace BTCPayServer.Views.Server
|
||||||
{
|
{
|
||||||
public enum ServerNavPages
|
public enum ServerNavPages
|
||||||
{
|
{
|
||||||
Index, Users, Rates, Emails, Policies, Theme, Hangfire
|
Index, Users, Rates, Emails, Policies, Theme, Hangfire, Services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
53
BTCPayServer/Views/Server/Services.cshtml
Normal file
53
BTCPayServer/Views/Server/Services.cshtml
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
@model BTCPayServer.Models.ServerViewModels.ServicesViewModel
|
||||||
|
@{
|
||||||
|
ViewData.SetActivePageAndTitle(ServerNavPages.Services);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
<h4>@ViewData["Title"]</h4>
|
||||||
|
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div asp-validation-summary="All" class="text-danger"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
@if(Model.LNDServices.Count != 0)
|
||||||
|
{
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="form-group">
|
||||||
|
<h5>LND nodes</h5>
|
||||||
|
<span>You can get access to internal LND services here. For gRPC, only BTC is supported.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<table class="table table-sm table-responsive-md">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Crypto</th>
|
||||||
|
<th>Access Type</th>
|
||||||
|
<th style="text-align:right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach(var lnd in Model.LNDServices)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@lnd.Crypto</td>
|
||||||
|
<td>@lnd.Type</td>
|
||||||
|
<td style="text-align:right">
|
||||||
|
<a asp-action="LNDGRPCServices" asp-route-cryptoCode="@lnd.Crypto">See information</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Rates)" asp-action="Rates">Rates</a>
|
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Rates)" asp-action="Rates">Rates</a>
|
||||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Emails)" asp-action="Emails">Email server</a>
|
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Emails)" asp-action="Emails">Email server</a>
|
||||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</a>
|
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</a>
|
||||||
|
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Services)" asp-action="Services">Services</a>
|
||||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
|
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
|
||||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Hangfire)" href="~/hangfire" target="_blank">Hangfire</a>
|
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Hangfire)" href="~/hangfire" target="_blank">Hangfire</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Reference in a new issue