Server Settings: Customize instance name and add contact URL (#5718)

* Server Settings: Customize instance name and add contact URL

- The custom instance name would improve #5563
- Added contact URL closes #4806

* Fix custom logo display
This commit is contained in:
d11n 2024-02-21 20:54:39 +01:00 committed by GitHub
parent 147c6c4548
commit 4ae1046571
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 206 additions and 102 deletions

View file

@ -252,7 +252,7 @@
{
<ul id="mainNavSettings" class="navbar-nav border-top p-3 px-lg-4">
<li class="nav-item" permission="@Policies.CanModifyServerSettings">
<a asp-area="" asp-controller="UIServer" asp-action="ListUsers" class="nav-link @ViewData.IsActivePage(ServerNavPages.Users) @ViewData.IsActivePage(ServerNavPages.Emails) @ViewData.IsActivePage(ServerNavPages.Policies) @ViewData.IsActivePage(ServerNavPages.Services) @ViewData.IsActivePage(ServerNavPages.Theme) @ViewData.IsActivePage(ServerNavPages.Maintenance) @ViewData.IsActivePage(ServerNavPages.Logs) @ViewData.IsActivePage(ServerNavPages.Files)" id="Nav-ServerSettings">
<a asp-area="" asp-controller="UIServer" asp-action="ListUsers" class="nav-link @ViewData.IsActivePage(ServerNavPages.Users) @ViewData.IsActivePage(ServerNavPages.Emails) @ViewData.IsActivePage(ServerNavPages.Policies) @ViewData.IsActivePage(ServerNavPages.Services) @ViewData.IsActivePage(ServerNavPages.Branding) @ViewData.IsActivePage(ServerNavPages.Maintenance) @ViewData.IsActivePage(ServerNavPages.Logs) @ViewData.IsActivePage(ServerNavPages.Files)" id="Nav-ServerSettings">
<vc:icon symbol="server-settings"/>
<span>Server Settings</span>
</a>

View file

@ -150,6 +150,7 @@ namespace BTCPayServer.Controllers
}
if (!ModelState.IsValid)
return View(vm);
if (command == "changedomain")
{
if (string.IsNullOrWhiteSpace(vm.DNSDomain))
@ -995,143 +996,167 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(Services));
}
[HttpGet("server/theme")]
public async Task<IActionResult> Theme()
[HttpGet("server/branding")]
public async Task<IActionResult> Branding()
{
var data = await _SettingsRepository.GetSettingAsync<ThemeSettings>() ?? new ThemeSettings();
return View(data);
var server = await _SettingsRepository.GetSettingAsync<ServerSettings>() ?? new ServerSettings();
var theme = await _SettingsRepository.GetSettingAsync<ThemeSettings>() ?? new ThemeSettings();
var vm = new BrandingViewModel
{
ServerName = server.ServerName,
ContactUrl = server.ContactUrl,
CustomTheme = theme.CustomTheme,
CustomThemeExtension = theme.CustomThemeExtension,
CustomThemeCssUri = theme.CustomThemeCssUri,
CustomThemeFileId = theme.CustomThemeFileId,
LogoFileId = theme.LogoFileId
};
return View(vm);
}
[HttpPost("server/theme")]
public async Task<IActionResult> Theme(
ThemeSettings model,
[HttpPost("server/branding")]
public async Task<IActionResult> Branding(
BrandingViewModel vm,
[FromForm] bool RemoveLogoFile,
[FromForm] bool RemoveCustomThemeFile)
{
var settingsChanged = false;
var settings = await _SettingsRepository.GetSettingAsync<ThemeSettings>() ?? new ThemeSettings();
var server = await _SettingsRepository.GetSettingAsync<ServerSettings>() ?? new ServerSettings();
var theme = await _SettingsRepository.GetSettingAsync<ThemeSettings>() ?? new ThemeSettings();
var userId = GetUserId();
if (userId is null)
return NotFound();
if (model.CustomThemeFile != null)
vm.LogoFileId = theme.LogoFileId;
vm.CustomThemeFileId = theme.CustomThemeFileId;
if (server.ServerName != vm.ServerName || server.ContactUrl != vm.ContactUrl)
{
if (model.CustomThemeFile.ContentType.Equals("text/css", StringComparison.InvariantCulture))
server.ServerName = vm.ServerName;
server.ContactUrl = vm.ContactUrl;
settingsChanged = true;
await _SettingsRepository.UpdateSetting(server);
}
if (vm.CustomThemeFile != null)
{
if (vm.CustomThemeFile.ContentType.Equals("text/css", StringComparison.InvariantCulture))
{
// delete existing file
if (!string.IsNullOrEmpty(settings.CustomThemeFileId))
if (!string.IsNullOrEmpty(theme.CustomThemeFileId))
{
await _fileService.RemoveFile(settings.CustomThemeFileId, userId);
await _fileService.RemoveFile(theme.CustomThemeFileId, userId);
}
// add new file
try
{
var storedFile = await _fileService.AddFile(model.CustomThemeFile, userId);
settings.CustomThemeFileId = storedFile.Id;
var storedFile = await _fileService.AddFile(vm.CustomThemeFile, userId);
vm.CustomThemeFileId = theme.CustomThemeFileId = storedFile.Id;
settingsChanged = true;
}
catch (Exception e)
{
ModelState.AddModelError(nameof(settings.CustomThemeFile), $"Could not save theme file: {e.Message}");
ModelState.AddModelError(nameof(vm.CustomThemeFile), $"Could not save theme file: {e.Message}");
}
}
else
{
ModelState.AddModelError(nameof(settings.CustomThemeFile), "The uploaded theme file needs to be a CSS file");
ModelState.AddModelError(nameof(vm.CustomThemeFile), "The uploaded theme file needs to be a CSS file");
}
}
else if (RemoveCustomThemeFile && !string.IsNullOrEmpty(settings.CustomThemeFileId))
else if (RemoveCustomThemeFile && !string.IsNullOrEmpty(theme.CustomThemeFileId))
{
await _fileService.RemoveFile(settings.CustomThemeFileId, userId);
settings.CustomThemeFileId = null;
await _fileService.RemoveFile(theme.CustomThemeFileId, userId);
vm.CustomThemeFileId = theme.CustomThemeFileId = null;
settingsChanged = true;
}
if (model.LogoFile != null)
if (vm.LogoFile != null)
{
if (model.LogoFile.Length > 1_000_000)
if (vm.LogoFile.Length > 1_000_000)
{
ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file should be less than 1MB");
ModelState.AddModelError(nameof(vm.LogoFile), "The uploaded logo file should be less than 1MB");
}
else if (!model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
else if (!vm.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
{
ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file needs to be an image");
ModelState.AddModelError(nameof(vm.LogoFile), "The uploaded logo file needs to be an image");
}
else
{
var formFile = await model.LogoFile.Bufferize();
var formFile = await vm.LogoFile.Bufferize();
if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))
{
ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file needs to be an image");
ModelState.AddModelError(nameof(vm.LogoFile), "The uploaded logo file needs to be an image");
}
else
{
model.LogoFile = formFile;
vm.LogoFile = formFile;
// delete existing file
if (!string.IsNullOrEmpty(settings.LogoFileId))
if (!string.IsNullOrEmpty(theme.LogoFileId))
{
await _fileService.RemoveFile(settings.LogoFileId, userId);
await _fileService.RemoveFile(theme.LogoFileId, userId);
}
// add new file
try
{
var storedFile = await _fileService.AddFile(model.LogoFile, userId);
settings.LogoFileId = storedFile.Id;
var storedFile = await _fileService.AddFile(vm.LogoFile, userId);
vm.LogoFileId = theme.LogoFileId = storedFile.Id;
settingsChanged = true;
}
catch (Exception e)
{
ModelState.AddModelError(nameof(settings.LogoFile), $"Could not save logo: {e.Message}");
ModelState.AddModelError(nameof(vm.LogoFile), $"Could not save logo: {e.Message}");
}
}
}
}
else if (RemoveLogoFile && !string.IsNullOrEmpty(settings.LogoFileId))
else if (RemoveLogoFile && !string.IsNullOrEmpty(theme.LogoFileId))
{
await _fileService.RemoveFile(settings.LogoFileId, userId);
settings.LogoFileId = null;
await _fileService.RemoveFile(theme.LogoFileId, userId);
vm.LogoFileId = theme.LogoFileId = null;
settingsChanged = true;
}
if (model.CustomTheme && !string.IsNullOrEmpty(model.CustomThemeCssUri) && !Uri.IsWellFormedUriString(model.CustomThemeCssUri, UriKind.RelativeOrAbsolute))
if (vm.CustomTheme && !string.IsNullOrEmpty(vm.CustomThemeCssUri) && !Uri.IsWellFormedUriString(vm.CustomThemeCssUri, UriKind.RelativeOrAbsolute))
{
ModelState.AddModelError(nameof(settings.CustomThemeCssUri), "Please provide a non-empty theme URI");
ModelState.AddModelError(nameof(theme.CustomThemeCssUri), "Please provide a non-empty theme URI");
}
else if (settings.CustomThemeCssUri != model.CustomThemeCssUri)
else if (theme.CustomThemeCssUri != vm.CustomThemeCssUri)
{
settings.CustomThemeCssUri = model.CustomThemeCssUri;
theme.CustomThemeCssUri = vm.CustomThemeCssUri;
settingsChanged = true;
}
if (settings.CustomThemeExtension != model.CustomThemeExtension)
if (theme.CustomThemeExtension != vm.CustomThemeExtension)
{
// Require a custom theme to be defined in that case
if (string.IsNullOrEmpty(model.CustomThemeCssUri) && string.IsNullOrEmpty(settings.CustomThemeFileId))
if (string.IsNullOrEmpty(vm.CustomThemeCssUri) && string.IsNullOrEmpty(theme.CustomThemeFileId))
{
ModelState.AddModelError(nameof(settings.CustomThemeFile), "Please provide a custom theme");
ModelState.AddModelError(nameof(vm.CustomThemeFile), "Please provide a custom theme");
}
else
{
settings.CustomThemeExtension = model.CustomThemeExtension;
theme.CustomThemeExtension = vm.CustomThemeExtension;
settingsChanged = true;
}
}
if (settings.CustomTheme != model.CustomTheme)
if (theme.CustomTheme != vm.CustomTheme)
{
settings.CustomTheme = model.CustomTheme;
theme.CustomTheme = vm.CustomTheme;
settingsChanged = true;
}
if (settingsChanged)
{
await _SettingsRepository.UpdateSetting(settings);
TempData[WellKnownTempData.SuccessMessage] = "Theme settings updated successfully";
await _SettingsRepository.UpdateSetting(theme);
TempData[WellKnownTempData.SuccessMessage] = "Settings updated successfully";
}
return View(settings);
return View(vm);
}
[Route("server/emails")]

View file

@ -0,0 +1,40 @@
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
namespace BTCPayServer.Models.ServerViewModels;
public class BrandingViewModel
{
// Server
[Display(Name = "Server Name")]
public string ServerName { get; set; }
[Display(Name = "Contact URL")]
public string ContactUrl { get; set; }
// Theme
[Display(Name = "Use custom theme")]
public bool CustomTheme { get; set; }
[Display(Name = "Custom Theme Extension Type")]
public ThemeExtension CustomThemeExtension { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
[MaxLength(500)]
[Display(Name = "Custom Theme CSS URL")]
public string CustomThemeCssUri { get; set; }
[Display(Name = "Custom Theme File")]
[JsonIgnore]
public IFormFile CustomThemeFile { get; set; }
public string CustomThemeFileId { get; set; }
[Display(Name = "Logo")]
[JsonIgnore]
public IFormFile LogoFile { get; set; }
public string LogoFileId { get; set; }
}

View file

@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
namespace BTCPayServer.Services;
public class ServerSettings
{
[Display(Name = "Server Name")]
public string ServerName { get; set; }
[Display(Name = "Contact URL")]
public string ContactUrl { get; set; }
}

View file

@ -1,5 +1,4 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
namespace BTCPayServer.Services;
@ -27,16 +26,8 @@ public class ThemeSettings
[Display(Name = "Custom Theme CSS URL")]
public string CustomThemeCssUri { get; set; }
[Display(Name = "Custom Theme File")]
[JsonIgnore]
public IFormFile CustomThemeFile { get; set; }
public string CustomThemeFileId { get; set; }
[Display(Name = "Logo")]
[JsonIgnore]
public IFormFile LogoFile { get; set; }
public string LogoFileId { get; set; }
public bool FirstRun { get; set; }

View file

@ -3,6 +3,7 @@
@using System.IO
@using BTCPayServer.Services
@inject IWebHostEnvironment WebHostEnvironment
@inject SettingsRepository SettingsRepository
@inject BTCPayServerEnvironment Env
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
@{
@ -12,6 +13,7 @@
async Task<string> GetDynamicManifest(string title)
{
var settings = await SettingsRepository.GetSettingAsync<ServerSettings>() ?? new ServerSettings();
var manifest = WebHostEnvironment.WebRootFileProvider.GetFileInfo("manifest.json");
if (!manifest.Exists)
{
@ -19,8 +21,9 @@
}
using var reader = new StreamReader(manifest.CreateReadStream());
var jObject = JObject.Parse(await reader.ReadToEndAsync());
var serverName = string.IsNullOrWhiteSpace(settings.ServerName) ? "BTCPay Server" : settings.ServerName;
jObject["short_name"] = title;
jObject["name"] = $"BTCPay Server: {title}";
jObject["name"] = $"{serverName}: {title}";
foreach (var jToken in jObject["icons"]!)
{
var icon = (JObject)jToken;

View file

@ -1,7 +1,13 @@
@using BTCPayServer.Services
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Components.MainLogo
@inject SettingsRepository SettingsRepository
@{
Layout = "_LayoutSimple";
ViewBag.ShowTitle ??= true;
ViewBag.ShowLeadText ??= false;
var settings = await SettingsRepository.GetSettingAsync<ServerSettings>() ?? new ServerSettings();
}
@section PageHeadContent {
@ -33,7 +39,7 @@
<vc:main-logo />
</a>
<h1 class="h2 mb-3">Welcome to your BTCPay&nbsp;Server</h1>
<h1 class="h2 mb-3">Welcome to @(string.IsNullOrWhiteSpace(settings.ServerName) ? "your BTCPay\u00a0Server" : settings.ServerName)</h1>
@if (ViewBag.ShowLeadText)
{
<p class="lead">
@ -49,11 +55,18 @@
<div class="account-form">
@if (ViewBag.ShowTitle)
{
<h4 v-pre>@ViewData["Title"]</h4>
<h4 v-pre>@ViewData["Title"]</h4>
}
@RenderBody()
</div>
@if (!string.IsNullOrWhiteSpace(settings.ContactUrl))
{
<p class="text-center mt-n5 mb-5 pt-2">
<a class="text-secondary" href="@settings.ContactUrl" id="ContactLink">Contact Us</a>
</p>
}
<div class="row justify-content-center mt-5">
<div class="col">
<partial name="_BTCPaySupporters"/>

View file

@ -3,7 +3,7 @@
}
<style>
.main-logo { height: 4rem; width: 18rem; }
.main-logo { height: 4.5rem; max-width: 18rem; }
.main-logo-btcpay .main-logo-btcpay--small { display: none; }
.lead img { max-width: 100%; }
</style>

View file

@ -1,11 +1,15 @@
@using BTCPayServer.Services
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model HomeViewModel;
@inject SettingsRepository SettingsRepository
@{
ViewData["Title"] = "BTCPay Server";
var settings = await SettingsRepository.GetSettingAsync<ServerSettings>() ?? new ServerSettings();
ViewData["Title"] = string.IsNullOrWhiteSpace(settings.ServerName) ? "BTCPay Server" : settings.ServerName;
}
<partial name="_StatusMessage" />
<h2>Welcome to your BTCPay Server</h2>
<h2>Welcome to @(string.IsNullOrWhiteSpace(settings.ServerName) ? "your BTCPay\u00a0Server" : settings.ServerName)</h2>
@if (!Model.HasStore)
{

View file

@ -1,9 +1,9 @@
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Services
@model BTCPayServer.Services.ThemeSettings
@model BrandingViewModel;
@inject IFileService FileService
@{
ViewData.SetActivePage(ServerNavPages.Theme, "Theme");
ViewData.SetActivePage(ServerNavPages.Branding, "Branding");
var canUpload = await FileService.IsAvailable();
var themeExtension = ((ThemeExtension[])Enum.GetValues(typeof(ThemeExtension))).Select(t =>
new SelectListItem(typeof(ThemeExtension).DisplayName(t.ToString()), t == ThemeExtension.Custom ? null : t.ToString()));
@ -18,6 +18,51 @@
<div class="row">
<div class="col-xl-8 col-xxl-constrain">
<form method="post" enctype="multipart/form-data">
<div class="form-group">
<label asp-for="ServerName" class="form-label"></label>
<input asp-for="ServerName" class="form-control" />
<div class="form-text">You can give this server a custom name, which will appear on public facing pages.</div>
<span asp-validation-for="ServerName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ContactUrl" class="form-label"></label>
<input asp-for="ContactUrl" class="form-control" />
<div class="form-text">
Contact link for support requests related to this BTCPay Server instance.
This link will appear on public facing pages.
Can be any valid URI, such as a website, email, and Nostr.
</div>
<span asp-validation-for="ContactUrl" class="text-danger"></span>
</div>
<div class="form-group">
<div class="d-flex align-items-center justify-content-between gap-2">
<label asp-for="LogoFile" class="form-label"></label>
@if (!string.IsNullOrEmpty(Model.LogoFileId))
{
<button type="submit" class="btn btn-link p-0 text-danger" name="RemoveLogoFile" value="true">
<span class="fa fa-times"></span> Remove
</button>
}
</div>
@if (canUpload)
{
<div class="d-flex align-items-center gap-3">
<input asp-for="LogoFile" type="file" class="form-control flex-grow">
@if (!string.IsNullOrEmpty(Model.LogoFileId))
{
<img src="@(await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.LogoFileId))" alt="Logo" style="height:2.1rem;max-width:10.5rem;"/>
}
</div>
<span asp-validation-for="LogoFile" class="text-danger"></span>
}
else
{
<input asp-for="LogoFile" type="file" class="form-control" disabled>
<div class="form-text">In order to upload a logo, a <a asp-controller="UIServer" asp-action="Files">file storage</a> must be configured.</div>
}
</div>
<h3 class="mt-5 mb-3">Theme</h3>
<p>Use the default Light or Dark Themes, or provide a custom CSS theme file below.</p>
<div class="d-flex align-items-center mb-3">
@ -75,35 +120,6 @@
}
</div>
<h3 class="mt-5 mb-3">Branding</h3>
<div class="form-group">
<div class="d-flex align-items-center justify-content-between gap-2">
<label asp-for="LogoFile" class="form-label"></label>
@if (!string.IsNullOrEmpty(Model.LogoFileId))
{
<button type="submit" class="btn btn-link p-0 text-danger" name="RemoveLogoFile" value="true">
<span class="fa fa-times"></span> Remove
</button>
}
</div>
@if (canUpload)
{
<div class="d-flex align-items-center gap-3">
<input asp-for="LogoFile" type="file" class="form-control flex-grow">
@if (!string.IsNullOrEmpty(Model.LogoFileId))
{
<img src="@(await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.LogoFileId))" alt="Logo" style="height:2.1rem;max-width:10.5rem;"/>
}
</div>
<span asp-validation-for="LogoFile" class="text-danger"></span>
}
else
{
<input asp-for="LogoFile" type="file" class="form-control" disabled>
<div class="form-text">In order to upload a logo, a <a asp-controller="UIServer" asp-action="Files">file storage</a> must be configured.</div>
}
</div>
<button type="submit" class="btn btn-primary mt-2" name="command" value="Save">Save</button>
</form>
</div>

View file

@ -13,10 +13,8 @@
<input asp-for="DNSDomain" class="form-control" disabled="@(Model.CanUseSSH ? null : "disabled")" />
<div class="form-text">You can change the domain name of your server by following <a href="https://docs.btcpayserver.org/FAQ/Deployment/#how-to-change-your-btcpay-server-domain-name" target="_blank" rel="noreferrer noopener">this guide</a>.</div>
<span asp-validation-for="DNSDomain" class="text-danger"></span>
<button name="command" type="submit" class="btn btn-secondary mt-3" value="changedomain" title="Change domain" disabled="@(Model.CanUseSSH ? null : "disabled")">
Confirm
</button>
</div>
<button name="command" type="submit" class="btn btn-secondary" value="changedomain" title="Change domain" disabled="@(Model.CanUseSSH ? null : "disabled")">Change Domain</button>
<h4 class="mt-5 mb-2">Restart</h4>
<p>Restart BTCPay Server and related services.</p>

View file

@ -2,7 +2,7 @@ namespace BTCPayServer.Views.Server
{
public enum ServerNavPages
{
Index, Users, Emails, Policies, Theme, Services, Maintenance, Logs, Files, Plugins,
Index, Users, Emails, Policies, Branding, Services, Maintenance, Logs, Files, Plugins,
Roles
}
}

View file

@ -10,7 +10,7 @@
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Emails" class="nav-link @ViewData.IsActivePage(ServerNavPages.Emails)" asp-action="Emails">Email</a>
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Policies" class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</a>
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Services" class="nav-link @ViewData.IsActivePage(ServerNavPages.Services)" asp-action="Services">Services</a>
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Theme" class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Branding" class="nav-link @ViewData.IsActivePage(ServerNavPages.Branding)" asp-action="Branding">Branding</a>
@if (_btcPayServerOptions.DockerDeployment)
{
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Maintenance" class="nav-link @ViewData.IsActivePage(ServerNavPages.Maintenance)" asp-action="Maintenance">Maintenance</a>

View file

@ -29,8 +29,8 @@
@section PageHeadContent {
<style>
#FirstStore { max-width: 27rem; margin: 0 auto; text-align: center; }
#FirstStore .main-logo { height: 4rem; width: 18rem; }
#FirstStore .main-logo.main-logo-btcpay { height: 4.5rem; width: 2.5rem; }
#FirstStore .main-logo { height: 4.5rem; max-width: 18rem; }
#FirstStore .main-logo.main-logo-btcpay { width: 2.5rem; }
#FirstStore .main-logo-btcpay .main-logo-btcpay--large { display: none; }
#FirstStore .form-control, #FirstStore .form-select { width: 100%; }
#FirstStore .form-text { font-size: var(--btcpay-font-size-s); }