mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-24 14:50:50 +01:00
* wip * Cleanups * UI updates * Update UIFormsController.cs * Make predefined forms usable statically * Add support for pos app + forms * pay request form rough support * invoice form through receipt page * Display form name in inherit from store setting * Do not request additional forms on invoice from pay request * fix up code * move checkoutform id in checkout appearance outside of checkotu v2 toggle * general fixes for form system * fix pav bug * UI updates * Fix warnings in Form builder (#4331) * Fix build warnings about string? Enable nullable on UIFormsController.cs Fixes CS8632 The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. * Clean up lack of space in injected services in Submit() of UIFormsController.cs * Remove unused variables (CS0219) and assignment of nullable value to nullable type (CS8600) * Cleanup double semicolons while we're at tit * Fix: If reverse proxy wasn't well configured, and error message should have been displayed (#4322) * fix monero issue * Server Settings: Update Policies page (#4326) Handles the multiple submit buttons on that page and closes #4319. Contains some UI unifications with other pages and also shows the block explorers without needing to toggle the section via JS. * Change confirmed to settled. (#4328) * POS: Fix null pointer Introduced in #4307, the referenced object needs to be `itemChoice` instead of `choice`. * Add documentation link to plugins (#4329) * Add documentation link to plugins * Minor UI updates Co-authored-by: Dennis Reimann <mail@dennisreimann.de> * Fix flaky test (#4330) * Fix flaky test * Update BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs Co-authored-by: d11n <mail@dennisreimann.de> Co-authored-by: d11n <mail@dennisreimann.de> * Remove invoice and store level form * add form test * fix migration for forms * fix * make pay request form submission redirect to invoice * Refactor FormQuery to only be able to query single store and single form * Put the Authorize at controller level on UIForms * Fix warnings * Fix ef request * Fix query to forms, ensure no permission bypass * Fix modify * Remove storeId from step form * Remove useless storeId parameter * Hide custom form feature in UI * Minor cleanups * Remove custom form options from select for now * More minor syntax cleanups * Update test * Add index - needs migration * Refactoring: Use PostRedirect instead of TempData for data transfer * Remove untested and unfinished code * formResponse should be a JObject, not a string * Fix case for Form type Co-authored-by: Dennis Reimann <mail@dennisreimann.de> Co-authored-by: JesterHodl <103882255+jesterhodl@users.noreply.github.com> Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com> Co-authored-by: Andreas Tasch <andy.tasch@gmail.com>
227 lines
12 KiB
C#
227 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using BTCPayServer.Abstractions.Constants;
|
|
using BTCPayServer.Abstractions.Extensions;
|
|
using BTCPayServer.Client;
|
|
using BTCPayServer.Client.Models;
|
|
using BTCPayServer.Data;
|
|
using BTCPayServer.Payments;
|
|
using BTCPayServer.Security;
|
|
using BTCPayServer.Services.Stores;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Cors;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using StoreData = BTCPayServer.Data.StoreData;
|
|
|
|
namespace BTCPayServer.Controllers.Greenfield
|
|
{
|
|
[ApiController]
|
|
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
|
[EnableCors(CorsPolicies.All)]
|
|
public class GreenfieldStoresController : ControllerBase
|
|
{
|
|
private readonly StoreRepository _storeRepository;
|
|
private readonly UserManager<ApplicationUser> _userManager;
|
|
|
|
public GreenfieldStoresController(StoreRepository storeRepository, UserManager<ApplicationUser> userManager)
|
|
{
|
|
_storeRepository = storeRepository;
|
|
_userManager = userManager;
|
|
}
|
|
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
|
[HttpGet("~/api/v1/stores")]
|
|
public Task<ActionResult<IEnumerable<Client.Models.StoreData>>> GetStores()
|
|
{
|
|
var stores = HttpContext.GetStoresData();
|
|
return Task.FromResult<ActionResult<IEnumerable<Client.Models.StoreData>>>(Ok(stores.Select(FromModel)));
|
|
}
|
|
|
|
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
|
[HttpGet("~/api/v1/stores/{storeId}")]
|
|
public IActionResult GetStore(string storeId)
|
|
{
|
|
var store = HttpContext.GetStoreData();
|
|
if (store == null)
|
|
{
|
|
return StoreNotFound();
|
|
}
|
|
return Ok(FromModel(store));
|
|
}
|
|
|
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
|
[HttpDelete("~/api/v1/stores/{storeId}")]
|
|
public async Task<IActionResult> RemoveStore(string storeId)
|
|
{
|
|
var store = HttpContext.GetStoreData();
|
|
if (store == null)
|
|
{
|
|
return StoreNotFound();
|
|
}
|
|
|
|
if (!_storeRepository.CanDeleteStores())
|
|
{
|
|
return this.CreateAPIError("unsupported",
|
|
"BTCPay Server is using a database server that does not allow you to remove stores.");
|
|
}
|
|
await _storeRepository.RemoveStore(storeId, _userManager.GetUserId(User));
|
|
return Ok();
|
|
}
|
|
|
|
[HttpPost("~/api/v1/stores")]
|
|
[Authorize(Policy = Policies.CanModifyStoreSettingsUnscoped, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
|
public async Task<IActionResult> CreateStore(CreateStoreRequest request)
|
|
{
|
|
var validationResult = Validate(request);
|
|
if (validationResult != null)
|
|
{
|
|
return validationResult;
|
|
}
|
|
|
|
var store = new Data.StoreData();
|
|
|
|
PaymentMethodId.TryParse(request.DefaultPaymentMethod, out var defaultPaymentMethodId);
|
|
ToModel(request, store, defaultPaymentMethodId);
|
|
await _storeRepository.CreateStore(_userManager.GetUserId(User), store);
|
|
return Ok(FromModel(store));
|
|
}
|
|
|
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
|
[HttpPut("~/api/v1/stores/{storeId}")]
|
|
public async Task<IActionResult> UpdateStore(string storeId, UpdateStoreRequest request)
|
|
{
|
|
var store = HttpContext.GetStoreData();
|
|
if (store == null)
|
|
{
|
|
return StoreNotFound();
|
|
}
|
|
var validationResult = Validate(request);
|
|
if (validationResult != null)
|
|
{
|
|
return validationResult;
|
|
}
|
|
|
|
PaymentMethodId.TryParse(request.DefaultPaymentMethod, out var defaultPaymentMethodId);
|
|
|
|
ToModel(request, store, defaultPaymentMethodId);
|
|
await _storeRepository.UpdateStore(store);
|
|
return Ok(FromModel(store));
|
|
}
|
|
|
|
private Client.Models.StoreData FromModel(Data.StoreData data)
|
|
{
|
|
var storeBlob = data.GetStoreBlob();
|
|
return new Client.Models.StoreData()
|
|
{
|
|
Id = data.Id,
|
|
Name = data.StoreName,
|
|
Website = data.StoreWebsite,
|
|
SpeedPolicy = data.SpeedPolicy,
|
|
DefaultPaymentMethod = data.GetDefaultPaymentId()?.ToStringNormalized(),
|
|
//blob
|
|
//we do not include DefaultCurrencyPairs,Spread, PreferredExchange, RateScripting, RateScript in this model and instead opt to set it in stores/storeid/rates endpoints
|
|
//we do not include ExcludedPaymentMethods in this model and instead opt to set it in stores/storeid/payment-methods endpoints
|
|
//we do not include EmailSettings in this model and instead opt to set it in stores/storeid/email endpoints
|
|
//we do not include PaymentMethodCriteria because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
|
|
NetworkFeeMode = storeBlob.NetworkFeeMode,
|
|
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
|
CheckoutType = storeBlob.CheckoutType,
|
|
Receipt = InvoiceDataBase.ReceiptOptions.Merge(storeBlob.ReceiptOptions, null),
|
|
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
|
|
LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints,
|
|
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback,
|
|
RedirectAutomatically = storeBlob.RedirectAutomatically,
|
|
LazyPaymentMethods = storeBlob.LazyPaymentMethods,
|
|
ShowRecommendedFee = storeBlob.ShowRecommendedFee,
|
|
RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget,
|
|
DefaultLang = storeBlob.DefaultLang,
|
|
MonitoringExpiration = storeBlob.MonitoringExpiration,
|
|
InvoiceExpiration = storeBlob.InvoiceExpiration,
|
|
CustomLogo = storeBlob.CustomLogo,
|
|
CustomCSS = storeBlob.CustomCSS,
|
|
HtmlTitle = storeBlob.HtmlTitle,
|
|
AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice,
|
|
LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate,
|
|
PaymentTolerance = storeBlob.PaymentTolerance,
|
|
PayJoinEnabled = storeBlob.PayJoinEnabled
|
|
};
|
|
}
|
|
|
|
private static void ToModel(StoreBaseData restModel, Data.StoreData model, PaymentMethodId defaultPaymentMethod)
|
|
{
|
|
var blob = model.GetStoreBlob();
|
|
model.StoreName = restModel.Name;
|
|
model.StoreName = restModel.Name;
|
|
model.StoreWebsite = restModel.Website;
|
|
model.SpeedPolicy = restModel.SpeedPolicy;
|
|
model.SetDefaultPaymentId(defaultPaymentMethod);
|
|
//we do not include the default payment method in this model and instead opt to set it in the stores/storeid/payment-methods endpoints
|
|
//blob
|
|
//we do not include DefaultCurrencyPairs;Spread; PreferredExchange; RateScripting; RateScript in this model and instead opt to set it in stores/storeid/rates endpoints
|
|
//we do not include ExcludedPaymentMethods in this model and instead opt to set it in stores/storeid/payment-methods endpoints
|
|
//we do not include EmailSettings in this model and instead opt to set it in stores/storeid/email endpoints
|
|
//we do not include OnChainMinValue and LightningMaxValue because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
|
|
blob.NetworkFeeMode = restModel.NetworkFeeMode;
|
|
blob.DefaultCurrency = restModel.DefaultCurrency;
|
|
blob.RequiresRefundEmail = restModel.RequiresRefundEmail;
|
|
blob.CheckoutType = restModel.CheckoutType;
|
|
blob.ReceiptOptions = InvoiceDataBase.ReceiptOptions.Merge(restModel.Receipt, null);
|
|
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi;
|
|
blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints;
|
|
blob.OnChainWithLnInvoiceFallback = restModel.OnChainWithLnInvoiceFallback;
|
|
blob.LazyPaymentMethods = restModel.LazyPaymentMethods;
|
|
blob.RedirectAutomatically = restModel.RedirectAutomatically;
|
|
blob.ShowRecommendedFee = restModel.ShowRecommendedFee;
|
|
blob.RecommendedFeeBlockTarget = restModel.RecommendedFeeBlockTarget;
|
|
blob.DefaultLang = restModel.DefaultLang;
|
|
blob.MonitoringExpiration = restModel.MonitoringExpiration;
|
|
blob.InvoiceExpiration = restModel.InvoiceExpiration;
|
|
blob.CustomLogo = restModel.CustomLogo;
|
|
blob.CustomCSS = restModel.CustomCSS;
|
|
blob.HtmlTitle = restModel.HtmlTitle;
|
|
blob.AnyoneCanInvoice = restModel.AnyoneCanCreateInvoice;
|
|
blob.LightningDescriptionTemplate = restModel.LightningDescriptionTemplate;
|
|
blob.PaymentTolerance = restModel.PaymentTolerance;
|
|
blob.PayJoinEnabled = restModel.PayJoinEnabled;
|
|
model.SetStoreBlob(blob);
|
|
}
|
|
|
|
private IActionResult Validate(StoreBaseData request)
|
|
{
|
|
if (request is null)
|
|
{
|
|
return BadRequest();
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(request.DefaultPaymentMethod) &&
|
|
!PaymentMethodId.TryParse(request.DefaultPaymentMethod, out var defaultPaymentMethodId))
|
|
{
|
|
ModelState.AddModelError(nameof(request.Name), "DefaultPaymentMethod is invalid");
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(request.Name))
|
|
ModelState.AddModelError(nameof(request.Name), "Name is missing");
|
|
else if (request.Name.Length < 1 || request.Name.Length > 50)
|
|
ModelState.AddModelError(nameof(request.Name), "Name can only be between 1 and 50 characters");
|
|
if (!string.IsNullOrEmpty(request.Website) && !Uri.TryCreate(request.Website, UriKind.Absolute, out _))
|
|
{
|
|
ModelState.AddModelError(nameof(request.Website), "Website is not a valid url");
|
|
}
|
|
if (request.InvoiceExpiration < TimeSpan.FromMinutes(1) && request.InvoiceExpiration > TimeSpan.FromMinutes(60 * 24 * 24))
|
|
ModelState.AddModelError(nameof(request.InvoiceExpiration), "InvoiceExpiration can only be between 1 and 34560 mins");
|
|
if (request.MonitoringExpiration < TimeSpan.FromMinutes(10) && request.MonitoringExpiration > TimeSpan.FromMinutes(60 * 24 * 24))
|
|
ModelState.AddModelError(nameof(request.MonitoringExpiration), "InvoiceExpiration can only be between 10 and 34560 mins");
|
|
if (request.PaymentTolerance < 0 && request.PaymentTolerance > 100)
|
|
ModelState.AddModelError(nameof(request.PaymentTolerance), "PaymentTolerance can only be between 0 and 100 percent");
|
|
|
|
return !ModelState.IsValid ? this.CreateValidationError(ModelState) : null;
|
|
}
|
|
|
|
private IActionResult StoreNotFound()
|
|
{
|
|
return this.CreateAPIError(404, "store-not-found", "The store was not found");
|
|
}
|
|
}
|
|
}
|