Multiple domains for apps in BTCPay

closes #887
This commit is contained in:
Kukks
2019-06-25 20:41:32 +02:00
parent a58ecfd35a
commit 6cab02cd99
7 changed files with 233 additions and 96 deletions

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Models; using BTCPayServer.Models;
@@ -10,6 +11,7 @@ using NBitcoin;
using Newtonsoft.Json; using Newtonsoft.Json;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
@@ -25,37 +27,58 @@ namespace BTCPayServer.Controllers
_cachedServerSettings = cachedServerSettings; _cachedServerSettings = cachedServerSettings;
} }
private async Task<ViewResult> GoToApp(string appId, AppType? appType)
{
if (appType.HasValue && !string.IsNullOrEmpty(appId))
{
switch (appType.Value)
{
case AppType.Crowdfund:
{
var serviceProvider = HttpContext.RequestServices;
var controller = (AppsPublicController)serviceProvider.GetService(typeof(AppsPublicController));
controller.Url = Url;
controller.ControllerContext = ControllerContext;
var res = await controller.ViewCrowdfund(appId, null) as ViewResult;
if (res != null)
{
res.ViewName = "/Views/AppsPublic/ViewCrowdfund.cshtml";
return res; // return
}
break;
}
case AppType.PointOfSale:
{
var serviceProvider = HttpContext.RequestServices;
var controller = (AppsPublicController)serviceProvider.GetService(typeof(AppsPublicController));
controller.Url = Url;
controller.ControllerContext = ControllerContext;
var res = await controller.ViewPointOfSale(appId) as ViewResult;
if (res != null)
{
res.ViewName = "/Views/AppsPublic/ViewPointOfSale.cshtml";
return res; // return
}
break;
}
}
}
return null;
}
public async Task<IActionResult> Index() public async Task<IActionResult> Index()
{ {
if (_cachedServerSettings.RootAppType is Services.Apps.AppType.Crowdfund) var matchedDomainMapping = _cachedServerSettings.DomainToAppMapping.FirstOrDefault(item =>
item.Domain.Equals(Request.Host.Host, StringComparison.InvariantCultureIgnoreCase));
if (matchedDomainMapping != null)
{ {
var serviceProvider = HttpContext.RequestServices; return await GoToApp(matchedDomainMapping.AppId, matchedDomainMapping.AppType) ?? View("Home");
var controller = (AppsPublicController)serviceProvider.GetService(typeof(AppsPublicController));
controller.Url = Url;
controller.ControllerContext = ControllerContext;
var res = await controller.ViewCrowdfund(_cachedServerSettings.RootAppId, null) as ViewResult;
if (res != null)
{
res.ViewName = "/Views/AppsPublic/ViewCrowdfund.cshtml";
return res; // return
}
}
else if (_cachedServerSettings.RootAppType is Services.Apps.AppType.PointOfSale)
{
var serviceProvider = HttpContext.RequestServices;
var controller = (AppsPublicController)serviceProvider.GetService(typeof(AppsPublicController));
controller.Url = Url;
controller.ControllerContext = ControllerContext;
var res = await controller.ViewPointOfSale(_cachedServerSettings.RootAppId) as ViewResult;
if (res != null)
{
res.ViewName = "/Views/AppsPublic/ViewPointOfSale.cshtml";
return res; // return
}
} }
return View("Home"); return await GoToApp(_cachedServerSettings.RootAppId, _cachedServerSettings.RootAppType) ?? View("Home");
} }
[Route("translate")] [Route("translate")]
@@ -116,20 +139,6 @@ namespace BTCPayServer.Controllers
return View(vm); return View(vm);
} }
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}
public IActionResult Contact()
{
ViewData["Message"] = "Your contact page.";
return View();
}
public IActionResult Error() public IActionResult Error()
{ {
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });

View File

@@ -17,6 +17,7 @@ using NBitcoin.DataEncoders;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
@@ -33,6 +34,7 @@ using BTCPayServer.Storage.Services.Providers;
using BTCPayServer.Services.Apps; using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using BTCPayServer.Data; using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
@@ -459,43 +461,61 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> Policies() public async Task<IActionResult> Policies()
{ {
var data = (await _SettingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings(); var data = (await _SettingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
await GetAppSelectList();
// load display app dropdown
using (var ctx = _ContextFactory.CreateContext())
{
var userId = _UserManager.GetUserId(base.User);
var selectList = ctx.Users.Where(user => user.Id == userId)
.SelectMany(s => s.UserStores)
.Select(s => s.StoreData)
.SelectMany(s => s.Apps)
.Select(a => new SelectListItem($"{a.AppType} - {a.Name}", a.Id)).ToList();
selectList.Insert(0, new SelectListItem("(None)", null));
ViewBag.AppsList = new SelectList(selectList, "Value", "Text", data.RootAppId);
}
return View(data); return View(data);
} }
[Route("server/policies")] [Route("server/policies")]
[HttpPost] [HttpPost]
public async Task<IActionResult> Policies(PoliciesSettings settings) public async Task<IActionResult> Policies(PoliciesSettings settings, string command = "")
{ {
if (!String.IsNullOrEmpty(settings.RootAppId)) await GetAppSelectList();
if (command == "add-domain")
{ {
using (var ctx = _ContextFactory.CreateContext()) ModelState.Clear();
{ settings.DomainToAppMapping.Add(new PoliciesSettings.DomainToAppMappingItem());
var app = ctx.Apps.SingleOrDefault(a => a.Id == settings.RootAppId); return View(settings);
if (app != null) }
settings.RootAppType = Enum.Parse<AppType>(app.AppType); if (command.StartsWith("remove-domain", StringComparison.InvariantCultureIgnoreCase))
else {
settings.RootAppType = null; ModelState.Clear();
} var index = int.Parse(command.Substring(command.IndexOf(":",StringComparison.InvariantCultureIgnoreCase) + 1), CultureInfo.InvariantCulture);
settings.DomainToAppMapping.RemoveAt(index);
return View(settings);
}
if (!ModelState.IsValid)
{
return View(settings);
}
var appIdsToFetch = settings.DomainToAppMapping.Select(item => item.AppId).ToList();
if (!string.IsNullOrEmpty(settings.RootAppId))
{
appIdsToFetch.Add(settings.RootAppId);
} }
else else
{ {
// not preserved on client side, but clearing it just in case
settings.RootAppType = null; settings.RootAppType = null;
} }
if (appIdsToFetch.Any())
{
using (var ctx = _ContextFactory.CreateContext())
{
var apps = await ctx.Apps.Where(data => appIdsToFetch.Contains(data.Id))
.ToDictionaryAsync(data => data.Id, data => Enum.Parse<AppType>(data.AppType));
if (!string.IsNullOrEmpty(settings.RootAppId))
{
settings.RootAppType = apps[settings.RootAppId];
}
foreach (var domainToAppMappingItem in settings.DomainToAppMapping)
{
domainToAppMappingItem.AppType = apps[domainToAppMappingItem.AppId];
}
}
}
await _SettingsRepository.UpdateSetting(settings); await _SettingsRepository.UpdateSetting(settings);
TempData["StatusMessage"] = "Policies updated successfully"; TempData["StatusMessage"] = "Policies updated successfully";
return RedirectToAction(nameof(Policies)); return RedirectToAction(nameof(Policies));
@@ -555,6 +575,22 @@ namespace BTCPayServer.Controllers
return View(result); return View(result);
} }
private async Task GetAppSelectList()
{
// load display app dropdown
using (var ctx = _ContextFactory.CreateContext())
{
var userId = _UserManager.GetUserId(base.User);
var selectList = await ctx.Users.Where(user => user.Id == userId)
.SelectMany(s => s.UserStores)
.Select(s => s.StoreData)
.SelectMany(s => s.Apps)
.Select(a => new SelectListItem($"{a.AppType} - {a.Name}", a.Id)).ToListAsync();
selectList.Insert(0, new SelectListItem("(None)", null));
ViewBag.AppsList = selectList;
}
}
private static bool TryParseAsExternalService(TorService torService, out ExternalService externalService) private static bool TryParseAsExternalService(TorService torService, out ExternalService externalService)
{ {
externalService = null; externalService = null;

View File

@@ -52,6 +52,8 @@ namespace BTCPayServer.HostedServices
public AppType? RootAppType { get; set; } public AppType? RootAppType { get; set; }
public string RootAppId { get; set; } public string RootAppId { get; set; }
public List<PoliciesSettings.DomainToAppMappingItem> DomainToAppMapping { get; set; }
internal void Update(PoliciesSettings data) internal void Update(PoliciesSettings data)
{ {
ShowRegister = !data.LockSubscription; ShowRegister = !data.LockSubscription;
@@ -59,6 +61,7 @@ namespace BTCPayServer.HostedServices
RootAppType = data.RootAppType; RootAppType = data.RootAppType;
RootAppId = data.RootAppId; RootAppId = data.RootAppId;
DomainToAppMapping = data.DomainToAppMapping;
} }
} }

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;

View File

@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Services.Apps; using BTCPayServer.Services.Apps;
using BTCPayServer.Validation;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace BTCPayServer.Services namespace BTCPayServer.Services
@@ -23,7 +24,16 @@ namespace BTCPayServer.Services
[Display(Name = "Display app on website root")] [Display(Name = "Display app on website root")]
public string RootAppId { get; set; } public string RootAppId { get; set; }
public AppType? RootAppType { get; set; } public AppType? RootAppType { get; set; }
public List<DomainToAppMappingItem> DomainToAppMapping { get; set; } = new List<DomainToAppMappingItem>();
public class DomainToAppMappingItem
{
[Display(Name = "Domain")][Required][HostName] public string Domain { get; set; }
[Display(Name = "App")][Required] public string AppId { get; set; }
public AppType AppType { get; set; }
}
} }
} }

View File

@@ -0,0 +1,23 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
namespace BTCPayServer.Validation
{
//from http://stackoverflow.com/questions/967516/ddg#967610
public class HostNameAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var str = value == null ? null : Convert.ToString(value, CultureInfo.InvariantCulture);
var valid = string.IsNullOrWhiteSpace(str) || Uri.CheckHostName(str) != UriHostNameType.Unknown;
if (!valid)
{
return new ValidationResult(ErrorMessage);
}
return ValidationResult.Success;
}
}
}

View File

@@ -4,39 +4,95 @@
} }
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" /> <partial name="_StatusMessage" for="@TempData["StatusMessage"]"/>
@if (!this.ViewContext.ModelState.IsValid) @if (!this.ViewContext.ModelState.IsValid)
{ {
<div class="row"> <div asp-validation-summary="All" class="text-danger"></div>
<div class="col-lg-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
} }
<div class="row">
<div class="col-lg-6"> <form method="post">
<form method="post"> <div class="form-group">
<div class="form-group"> <label asp-for="RequiresConfirmedEmail"></label>
<label asp-for="RequiresConfirmedEmail"></label> <input asp-for="RequiresConfirmedEmail" type="checkbox" class="form-check-inline"/>
<input asp-for="RequiresConfirmedEmail" type="checkbox" class="form-check-inline" /> <span asp-validation-for="RequiresConfirmedEmail" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="LockSubscription"></label>
<input asp-for="LockSubscription" type="checkbox" class="form-check-inline" />
</div>
<div class="form-group">
<label asp-for="DiscourageSearchEngines"></label>
<input asp-for="DiscourageSearchEngines" type="checkbox" class="form-check-inline" />
</div>
<div class="form-group">
<label asp-for="RootAppId"></label>
<select asp-for="RootAppId" asp-items="ViewBag.AppsList" class="form-control"></select>
</div>
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
</form>
</div> </div>
</div> <div class="form-group">
<label asp-for="LockSubscription"></label>
<input asp-for="LockSubscription" type="checkbox" class="form-check-inline"/>
<span asp-validation-for="LockSubscription" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DiscourageSearchEngines"></label>
<input asp-for="DiscourageSearchEngines" type="checkbox" class="form-check-inline"/>
<span asp-validation-for="DiscourageSearchEngines" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="RootAppId"></label>
<select asp-for="RootAppId" asp-items="@(new SelectList(ViewBag.AppsList, nameof(SelectListItem.Value), nameof(SelectListItem.Text), Model.RootAppId))" class="form-control"></select>
@if (!Model.DomainToAppMapping.Any())
{
<button type="submit" name="command" value="add-domain" class="btn btn-link"> Map specific domains to specific apps</button>
}
</div>
@if (Model.DomainToAppMapping.Any())
{
<div class="list-group mb-2">
<div class="list-group-item">
<h5 class="mb-1">
Domain to app mapping
<button type="submit" name="command" value="add-domain" class="ml-1 btn btn-secondary btn-sm ">Add domain mapping </button>
</h5>
</div>
@for (var index = 0; index < Model.DomainToAppMapping.Count; index++)
{
<div class="list-group-item p-0 pl-lg-2">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-10 py-2 ">
<div class="form-group">
<label asp-for="DomainToAppMapping[index].Domain" class="control-label"></label>
<input asp-for="DomainToAppMapping[index].Domain" class="form-control"/>
<span asp-validation-for="DomainToAppMapping[index].Domain" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DomainToAppMapping[index].AppId"></label>
<select asp-for="DomainToAppMapping[index].AppId"
asp-items="@(new SelectList(ViewBag.AppsList,
nameof(SelectListItem.Value),
nameof(SelectListItem.Text),
Model.DomainToAppMapping[index].AppId))"
class="form-control">
</select>
<span asp-validation-for="DomainToAppMapping[index].AppId" class="text-danger"></span>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-2 pull-right">
<button type="submit" title="Remove domain mapping" name="command" value="@($"remove-domain:{index}")"
class="d-block d-lg-none d-xl-none btn btn-danger mb-2 ml-2">
Remove Destination
</button>
<button type="submit" title="Remove domain mapping" name="command" value="@($"remove-domain:{index}")"
class="d-none d-lg-block remove-domain-btn text-decoration-none h-100 align-middle btn text-danger btn-link fa fa-times rounded-0 pull-right">
</button>
</div>
</div>
</div>
}
</div>
}
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
</form>
@section Scripts { @section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial") <style>
.remove-domain-btn{
font-size: 1.5rem;
border-radius: 0;
}
.remove-domain-btn:hover{
background-color: #CCCCCC;
}
</style>
} }