btcpayserver/BTCPayServer/Plugins/Crowdfund/Controllers/UICrowdfundController.cs
Umar Bolatov 7106830be9
Make sure end date is after start date in Crowdfund app (#4084)
* Make sure end date is after start date in Crowdfund app

* Add null checks

* Add test case

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2022-09-09 19:26:11 +09:00

405 lines
17 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Plugins.Crowdfund.Models;
using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
using NicolasDorier.RateLimits;
namespace BTCPayServer.Plugins.Crowdfund.Controllers
{
[AutoValidateAntiforgeryToken]
[Route("apps")]
public class UICrowdfundController : Controller
{
public UICrowdfundController(
AppService appService,
CurrencyNameTable currencies,
EventAggregator eventAggregator,
StoreRepository storeRepository,
UIInvoiceController invoiceController,
UserManager<ApplicationUser> userManager)
{
_currencies = currencies;
_appService = appService;
_userManager = userManager;
_storeRepository = storeRepository;
_eventAggregator = eventAggregator;
_invoiceController = invoiceController;
}
private readonly EventAggregator _eventAggregator;
private readonly CurrencyNameTable _currencies;
private readonly StoreRepository _storeRepository;
private readonly AppService _appService;
private readonly UIInvoiceController _invoiceController;
private readonly UserManager<ApplicationUser> _userManager;
[HttpGet("/")]
[HttpGet("/apps/{appId}/crowdfund")]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[DomainMappingConstraint(AppType.Crowdfund)]
public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
{
var app = await _appService.GetApp(appId, AppType.Crowdfund, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var isAdmin = await _appService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency);
if (!hasEnoughSettingsToLoad)
{
if (!isAdmin)
return NotFound();
return NotFound("A Target Currency must be set for this app in order to be loadable.");
}
var appInfo = await GetAppInfo(appId);
if (settings.Enabled)
return View("Crowdfund/Public/ViewCrowdfund", appInfo);
if (!isAdmin)
return NotFound();
return View("Crowdfund/Public/ViewCrowdfund", appInfo);
}
[HttpPost("/")]
[HttpPost("/apps/{appId}/crowdfund")]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
[DomainMappingConstraint(AppType.Crowdfund)]
[RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken)
{
var app = await _appService.GetApp(appId, AppType.Crowdfund, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var isAdmin = await _appService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
if (!settings.Enabled && !isAdmin)
{
return NotFound("Crowdfund is not currently active");
}
var info = await GetAppInfo(appId);
if (!isAdmin &&
((settings.StartDate.HasValue && DateTime.UtcNow < settings.StartDate) ||
(settings.EndDate.HasValue && DateTime.UtcNow > settings.EndDate) ||
(settings.EnforceTargetAmount &&
(info.Info.PendingProgressPercentage.GetValueOrDefault(0) +
info.Info.ProgressPercentage.GetValueOrDefault(0)) >= 100)))
{
return NotFound("Crowdfund is not currently active");
}
var store = await _appService.GetStore(app);
var title = settings.Title;
decimal? price = request.Amount;
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(request.ChoiceKey))
{
var choices = _appService.GetPOSItems(settings.PerksTemplate, settings.TargetCurrency);
choice = choices?.FirstOrDefault(c => c.Id == request.ChoiceKey);
if (choice == null)
return NotFound("Incorrect option provided");
title = choice.Title;
if (choice.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup)
{
price = null;
}
else
{
price = choice.Price.Value;
if (request.Amount > price)
price = request.Amount;
}
if (choice.Inventory.HasValue)
{
if (choice.Inventory <= 0)
{
return NotFound("Option was out of stock");
}
}
if (choice?.PaymentMethods?.Any() is true)
{
paymentMethods = choice?.PaymentMethods.ToDictionary(s => s,
s => new InvoiceSupportedTransactionCurrency() { Enabled = true });
}
}
else
{
if (request.Amount < 0)
{
return NotFound("Please provide an amount greater than 0");
}
price = request.Amount;
}
if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price >
(info.TargetAmount - (info.Info.CurrentAmount + info.Info.CurrentPendingAmount))))
{
return NotFound("Contribution Amount is more than is currently allowed.");
}
try
{
var invoice = await _invoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest()
{
OrderId = AppService.GetAppOrderId(app),
Currency = settings.TargetCurrency,
ItemCode = request.ChoiceKey ?? string.Empty,
ItemDesc = title,
BuyerEmail = request.Email,
Price = price,
NotificationURL = settings.NotificationUrl,
FullNotifications = true,
ExtendedNotifications = true,
SupportedTransactionCurrencies = paymentMethods,
RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl(),
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string>() {AppService.GetAppInternalTag(appId)},
cancellationToken, (entity) =>
{
entity.Metadata.OrderUrl = Request.GetDisplayUrl();
});
if (request.RedirectToCheckout)
{
return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice",
new {invoiceId = invoice.Data.Id});
}
return Ok(invoice.Data.Id);
}
catch (BitpayHttpException e)
{
return BadRequest(e.Message);
}
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId)
{
var app = GetCurrentApp();
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var resetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery);
var vm = new UpdateCrowdfundViewModel
{
Title = settings.Title,
StoreId = app.StoreDataId,
StoreName = app.StoreData?.StoreName,
StoreDefaultCurrency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, settings.TargetCurrency),
AppName = app.Name,
Enabled = settings.Enabled,
EnforceTargetAmount = settings.EnforceTargetAmount,
StartDate = settings.StartDate,
TargetCurrency = settings.TargetCurrency,
Description = settings.Description,
MainImageUrl = settings.MainImageUrl,
EmbeddedCSS = settings.EmbeddedCSS,
EndDate = settings.EndDate,
TargetAmount = settings.TargetAmount,
CustomCSSLink = settings.CustomCSSLink,
NotificationUrl = settings.NotificationUrl,
Tagline = settings.Tagline,
PerksTemplate = settings.PerksTemplate,
DisqusEnabled = settings.DisqusEnabled,
SoundsEnabled = settings.SoundsEnabled,
DisqusShortname = settings.DisqusShortname,
AnimationsEnabled = settings.AnimationsEnabled,
ResetEveryAmount = settings.ResetEveryAmount,
ResetEvery = resetEvery,
IsRecurring = resetEvery != nameof(CrowdfundResetEvery.Never),
UseAllStoreInvoices = app.TagAllInvoices,
AppId = appId,
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetAppOrderId(app)}",
DisplayPerksRanking = settings.DisplayPerksRanking,
DisplayPerksValue = settings.DisplayPerksValue,
SortPerksByPopularity = settings.SortPerksByPopularity,
Sounds = string.Join(Environment.NewLine, settings.Sounds),
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors)
};
return View("Crowdfund/UpdateCrowdfund", vm);
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpPost("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm, string command)
{
var app = GetCurrentApp();
if (app == null)
return NotFound();
vm.TargetCurrency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, vm.TargetCurrency);
if (_currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");
try
{
vm.PerksTemplate = _appService.SerializeTemplate(_appService.Parse(vm.PerksTemplate, vm.TargetCurrency));
}
catch
{
ModelState.AddModelError(nameof(vm.PerksTemplate), "Invalid template");
}
if (vm.TargetAmount is decimal v && v == 0.0m)
{
vm.TargetAmount = null;
}
if (!vm.IsRecurring)
{
vm.ResetEvery = nameof(CrowdfundResetEvery.Never);
}
if (Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery) != CrowdfundResetEvery.Never && !vm.StartDate.HasValue)
{
ModelState.AddModelError(nameof(vm.StartDate), "A start date is needed when the goal resets every X amount of time");
}
if (Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery) != CrowdfundResetEvery.Never && vm.ResetEveryAmount <= 0)
{
ModelState.AddModelError(nameof(vm.ResetEveryAmount), "You must reset the goal at a minimum of 1");
}
if (vm.StartDate != null && vm.EndDate != null && DateTime.Compare((DateTime)vm.StartDate, (DateTime)vm.EndDate) > 0)
{
ModelState.AddModelError(nameof(vm.EndDate), "End date cannot be before start date");
}
if (vm.DisplayPerksRanking)
{
vm.SortPerksByPopularity = true;
}
var parsedSounds = vm.Sounds?.Split(
new[] { "\r\n", "\r", "\n" },
StringSplitOptions.None
).Select(s => s.Trim()).ToArray();
if (vm.SoundsEnabled && (parsedSounds == null || !parsedSounds.Any()))
{
vm.SoundsEnabled = false;
parsedSounds = new CrowdfundSettings().Sounds;
}
var parsedAnimationColors = vm.AnimationColors?.Split(
new[] { "\r\n", "\r", "\n" },
StringSplitOptions.None
).Select(s => s.Trim()).ToArray();
if (vm.AnimationsEnabled && (parsedAnimationColors == null || !parsedAnimationColors.Any()))
{
vm.AnimationsEnabled = false;
parsedAnimationColors = new CrowdfundSettings().AnimationColors;
}
if (!ModelState.IsValid)
{
return View("Crowdfund/UpdateCrowdfund", vm);
}
app.Name = vm.AppName;
var newSettings = new CrowdfundSettings
{
Title = vm.Title,
Enabled = vm.Enabled,
EnforceTargetAmount = vm.EnforceTargetAmount,
StartDate = vm.StartDate?.ToUniversalTime(),
TargetCurrency = vm.TargetCurrency,
Description = vm.Description,
EndDate = vm.EndDate?.ToUniversalTime(),
TargetAmount = vm.TargetAmount,
CustomCSSLink = vm.CustomCSSLink,
MainImageUrl = vm.MainImageUrl,
EmbeddedCSS = vm.EmbeddedCSS,
NotificationUrl = vm.NotificationUrl,
Tagline = vm.Tagline,
PerksTemplate = vm.PerksTemplate,
DisqusEnabled = vm.DisqusEnabled,
SoundsEnabled = vm.SoundsEnabled,
DisqusShortname = vm.DisqusShortname,
AnimationsEnabled = vm.AnimationsEnabled,
ResetEveryAmount = vm.ResetEveryAmount,
ResetEvery = Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery),
DisplayPerksValue = vm.DisplayPerksValue,
DisplayPerksRanking = vm.DisplayPerksRanking,
SortPerksByPopularity = vm.SortPerksByPopularity,
Sounds = parsedSounds,
AnimationColors = parsedAnimationColors
};
app.TagAllInvoices = vm.UseAllStoreInvoices;
app.SetSettings(newSettings);
await _appService.UpdateOrCreateApp(app);
_eventAggregator.Publish(new UIAppsController.AppUpdated
{
AppId = appId,
StoreId = app.StoreDataId,
Settings = newSettings
});
TempData[WellKnownTempData.SuccessMessage] = "App updated";
return RedirectToAction(nameof(UpdateCrowdfund), new { appId });
}
private async Task<string> GetStoreDefaultCurrentIfEmpty(string storeId, string currency)
{
if (string.IsNullOrWhiteSpace(currency))
{
var store = await _storeRepository.FindStore(storeId);
if (store == null)
{
throw new Exception($"Could not find store with id {storeId}");
}
currency = store.GetStoreBlob().DefaultCurrency;
}
return currency.Trim().ToUpperInvariant();
}
private AppData GetCurrentApp() => HttpContext.GetAppData();
private string GetUserId() => _userManager.GetUserId(User);
private async Task<ViewCrowdfundViewModel> GetAppInfo(string appId)
{
var info = (ViewCrowdfundViewModel)await _appService.GetAppInfo(appId);
info.HubPath = AppHub.GetHubPath(Request);
info.SimpleDisplay = Request.Query.ContainsKey("simple");
return info;
}
}
}