Unnest UI views of PullRequests, PullPayments, Invoices and Apps (#7368)
What changed, and why it matters
This commit restructures several user-interface pages so that payment requests, pull payments, invoices and apps no longer live under a '/stores/{storeId}/...' URL path. Instead they use flatter routes such as '/payment-requests/{id}/edit'. The change also updates the authorization layer so that when a store-scoped route is missing but an object ID is present, the system can fail the request with a 403 rather than letting the action itself silently return a 404. Several tests were updated to expect 403 instead of 404 for missing objects, and some permission-guard tests were removed or changed. The commit is described by the author as a UI route cleanup, not as a security fix, but it does touch access-control code.
Treat this as a routine refactor with authorization hardening. Review that all newly added flat routes still enforce the correct policies and that the implicit scope providers correctly resolve the store for every object type exposed without a storeId route segment. Run the updated integration tests and verify that 403 responses do not leak existence of resources to unauthorized users in a way that violates the application's threat model. No emergency patch is indicated.
Security signals we found
Authorization scope provider changed to fail closed (403) when store context cannot be resolved
Permission handler now respects context.HasFailed and short-circuits
Tests updated to expect 403 instead of 404 for missing resources
UI routes decoupled from explicit storeId path parameter
Removed direct NotFound permission-guard test for guest editing payment request
Evidence from the diff
The patch ‘unnests’ MVC routes for PaymentRequests, PullPayments, Invoices and Apps. Controllers now read the current store from HttpContext.GetStoreData() instead of a route-bound storeId parameter, and routes are added without the stores/{storeId} prefix. BuiltInPermissionScopeProvider.cs is modified: when storeId is null on a non-legacy route it calls authContext.Fail() (returning 403), and when the resolved storeId from an object scope does not match the route storeId it also fails. PermissionAuthorizationHandler.cs now short-circuits if context.HasFailed after implicit scope resolution. Tests change expected status codes from 404 to 403 for non-existent apps, payment requests and invoices, and remove a direct ‘guest editing returns NotFound’ assertion. The change is structural; it does not add new input validation or fix a clearly identified vulnerability, but it does harden the authorization boundary by failing closed when scope resolution cannot validate the store.
Changed components
BTCPayServer/Security/BuiltInPermissionScopeProvider.csBTCPayServer/Security/PermissionAuthorizationHandler.csBTCPayServer/Controllers/UIPaymentRequestController.csBTCPayServer/Controllers/UIPullPaymentController.csBTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.csBTCPayServer/Controllers/UIInvoiceController.UI.csBTCPayServer/Controllers/UIAppsController.csBTCPayServer/Views/UIPaymentRequest/EditPaymentRequest.cshtmlBTCPayServer/Views/UIPaymentRequest/GetPaymentRequests.cshtmlBTCPayServer/Views/UIPaymentRequest/ViewPaymentRequest.cshtmlBTCPayServer/Views/UIPullPayment/EditPullPayment.cshtmlBTCPayServer/Views/UIPullPayment/ViewPullPayment.cshtmlBTCPayServer/Views/UIStorePullPayments/Payouts.cshtmlBTCPayServer/Views/UIStorePullPayments/PullPayments.cshtmlInspect captured patch +181 / −224
diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs
index f04edb8..fe33b99 100644
--- a/BTCPayServer.Tests/GreenfieldAPITests.cs
+++ b/BTCPayServer.Tests/GreenfieldAPITests.cs
@@ -406,8 +406,8 @@ namespace BTCPayServer.Tests
Assert.False(app.ShowCategories);
Assert.False(app.ShowDiscount);
- // Make sure we return a 404 if we try to get an app that doesn't exist
- await AssertHttpError(404, async () =>
+ // Make sure we return a 403 if we try to get an app that doesn't exist
+ await AssertHttpError(403, async () =>
{
await client.GetApp("some random ID lol");
});
@@ -446,8 +446,8 @@ namespace BTCPayServer.Tests
Assert.Equal("new app name", retrievedPosApp.AppName);
Assert.Equal("new app title", retrievedPosApp.Title);
- // Make sure we return a 404 if we try to delete an app that doesn't exist
- await AssertHttpError(404, async () =>
+ // Make sure we return a 403 if we try to delete an app that doesn't exist
+ await AssertHttpError(403, async () =>
{
await client.DeleteApp("some random ID lol");
});
@@ -612,7 +612,7 @@ namespace BTCPayServer.Tests
Assert.Equal("test app name", app.Title);
// Make sure we return a 404 if we try to get an app that doesn't exist
- await AssertHttpError(404, async () =>
+ await AssertHttpError(403, async () =>
{
await client.GetApp("some random ID lol");
});
@@ -695,8 +695,8 @@ namespace BTCPayServer.Tests
Assert.Equal(stateBeforeFailedPut.Title, stateAfterFailedPut.Title);
Assert.Equal(stateBeforeFailedPut.Description, stateAfterFailedPut.Description);
- // Make sure we return a 404 if we try to delete an app that doesn't exist
- await AssertHttpError(404, async () =>
+ // Make sure we return a 403 if we try to delete an app that doesn't exist
+ await AssertHttpError(403, async () =>
{
await client.DeleteApp("some random ID lol");
});
@@ -1677,7 +1677,7 @@ namespace BTCPayServer.Tests
});
// test validation that the invoice exists
- await AssertHttpError(404, async () =>
+ await AssertHttpError(403, async () =>
{
await client.RefundInvoice("lol fake invoice id", new RefundInvoiceRequest()
{
diff --git a/BTCPayServer.Tests/PaymentRequestTests.cs b/BTCPayServer.Tests/PaymentRequestTests.cs
index 8bdda8e..91d1b4d 100644
--- a/BTCPayServer.Tests/PaymentRequestTests.cs
+++ b/BTCPayServer.Tests/PaymentRequestTests.cs
@@ -209,14 +209,13 @@ namespace BTCPayServer.Tests
await user2.GrantAccessAsync();
var paymentRequestController = user.GetController<UIPaymentRequestController>();
- var guestpaymentRequestController = user2.GetController<UIPaymentRequestController>();
+ var storeId = user.StoreId;
var request = new UpdatePaymentRequestViewModel
{
Title = "original juice",
Currency = "BTC",
Amount = 1,
- StoreId = user.StoreId,
Description = "description",
ReferenceId = "custom-id-1"
};
@@ -230,10 +229,7 @@ namespace BTCPayServer.Tests
Assert.Equal("original juice", prData.Title);
Assert.Equal("custom-id-1", prData.ReferenceId);
- paymentRequestController.HttpContext.SetPaymentRequestData(new PaymentRequestData { Id = id, StoreDataId = request.StoreId });
-
- // Permission guard for guests editing
- (await guestpaymentRequestController.EditPaymentRequest(user.StoreId, id)).AssertType<NotFoundResult>();
+ paymentRequestController.HttpContext.SetPaymentRequestData(new PaymentRequestData { Id = id, StoreDataId = storeId });
request.Title = "update";
request.ReferenceId = "custom-id-2";
@@ -282,6 +278,7 @@ namespace BTCPayServer.Tests
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
+ var storeId = user.StoreId;
var paymentRequestController = user.GetController<UIPaymentRequestController>();
// Create first payment request with ReferenceId
@@ -290,7 +287,6 @@ namespace BTCPayServer.Tests
Title = "First Payment Request",
Currency = "BTC",
Amount = 1,
- StoreId = user.StoreId,
Description = "First request",
ReferenceId = "duplicate-ref-id"
};
@@ -306,7 +302,6 @@ namespace BTCPayServer.Tests
Title = "Second Payment Request",
Currency = "BTC",
Amount = 2,
- StoreId = user.StoreId,
Description = "Second request",
ReferenceId = "duplicate-ref-id"
};
@@ -318,7 +313,7 @@ namespace BTCPayServer.Tests
// Try to edit first payment request to use a different ReferenceId - should succeed
paymentRequestController.ModelState.Clear();
- paymentRequestController.HttpContext.SetPaymentRequestData(new PaymentRequestData { Id = id1, StoreDataId = request1.StoreId });
+ paymentRequestController.HttpContext.SetPaymentRequestData(new PaymentRequestData { Id = id1, StoreDataId = storeId });
request1.ReferenceId = "new-unique-ref-id";
(await paymentRequestController.EditPaymentRequest(id1, request1)).AssertType<RedirectToActionResult>();
@@ -331,7 +326,7 @@ namespace BTCPayServer.Tests
// Try to edit second payment request to use first payment request's current ReferenceId - should fail
paymentRequestController.ModelState.Clear();
- paymentRequestController.HttpContext.SetPaymentRequestData(new PaymentRequestData { Id = id2, StoreDataId = request2.StoreId });
+ paymentRequestController.HttpContext.SetPaymentRequestData(new PaymentRequestData { Id = id2, StoreDataId = storeId });
request2.ReferenceId = "new-unique-ref-id";
result = await paymentRequestController.EditPaymentRequest(id2, request2);
viewResult = result.AssertType<ViewResult>();
@@ -360,7 +355,6 @@ namespace BTCPayServer.Tests
Title = "original juice",
Currency = "BTC",
Amount = 1,
- StoreId = user.StoreId,
Description = "description",
ExpiryDate = (DateTimeOffset.UtcNow + TimeSpan.FromDays(1.0)).UtcDateTime
};
@@ -396,7 +390,6 @@ namespace BTCPayServer.Tests
Currency = "BTC",
Amount = 1,
ExpiryDate = DateTime.Today.Subtract(TimeSpan.FromDays(2)),
- StoreId = user.StoreId,
Description = "description"
};
@@ -430,7 +423,6 @@ namespace BTCPayServer.Tests
Title = "original juice",
Currency = "BTC",
Amount = 1,
- StoreId = user.StoreId,
Description = "description"
};
var response = paymentRequestController.EditPaymentRequest(null, request).Result
diff --git a/BTCPayServer.Tests/PlaywrightTests.cs b/BTCPayServer.Tests/PlaywrightTests.cs
index e38994e..2a6d129 100644
--- a/BTCPayServer.Tests/PlaywrightTests.cs
+++ b/BTCPayServer.Tests/PlaywrightTests.cs
@@ -250,7 +250,7 @@ namespace BTCPayServer.Tests
Assert.False(await s.Page.IsEnabledAsync("#Currency"));
// archive (from details page)
- var payReqId = s.Page.Url.Split('/').Last();
+ var payReqId = s.Page.Url.Split('/')[^2];
await s.Page.ClickAsync("#ArchivePaymentRequest");
await s.FindAlertMessage(partialText: "The payment request has been archived");
Assert.DoesNotContain("Pay123", await s.Page.ContentAsync());
@@ -285,12 +285,15 @@ namespace BTCPayServer.Tests
// Mine
- await checkoutFrame.Locator("#mine-block button").ClickAsync();
- await checkoutFrame.Locator("#CheatSuccessMessage").WaitForAsync();
- Assert.Contains("Mined 1 block", await checkoutFrame.Locator("#CheatSuccessMessage").InnerTextAsync());
+ await s.Server.WaitForEvent<BTCPayServer.Services.PaymentRequests.PaymentRequestEvent>(async () =>
+ {
+ await checkoutFrame.Locator("#mine-block button").ClickAsync();
+ await checkoutFrame.Locator("#CheatSuccessMessage").WaitForAsync();
+ Assert.Contains("Mined 1 block", await checkoutFrame.Locator("#CheatSuccessMessage").InnerTextAsync());
- await checkoutFrame.Locator("#close").ClickAsync();
- await s.Page.Locator("iframe[name='btcpay']").WaitForAsync(new() { State = WaitForSelectorState.Detached });
+ await checkoutFrame.Locator("#close").ClickAsync();
+ await s.Page.Locator("iframe[name='btcpay']").WaitForAsync(new() { State = WaitForSelectorState.Detached });
+ }, ev => ev.Data.Status == PaymentRequestStatus.Completed);
// One last refresh to ensure UI reflects final state
await s.Page.ReloadAsync();
@@ -2199,7 +2202,7 @@ namespace BTCPayServer.Tests
await s.AssertPageAccess(true, GetStorePath("invoices"));
await s.AssertPageAccess(false, GetStorePath("invoices/create"));
await s.AssertPageAccess(true, GetStorePath("payment-requests"));
- await s.AssertPageAccess(false, GetStorePath("payment-requests/edit"));
+ await s.AssertPageAccess(false, GetStorePath("payment-requests/new"));
await s.AssertPageAccess(true, GetStorePath("pull-payments"));
await s.AssertPageAccess(true, GetStorePath("payouts"));
await s.AssertPageAccess(false, GetStorePath("onchain/BTC"));
diff --git a/BTCPayServer.Tests/RolesTests.cs b/BTCPayServer.Tests/RolesTests.cs
index ebd040e..bb4eb33 100644
--- a/BTCPayServer.Tests/RolesTests.cs
+++ b/BTCPayServer.Tests/RolesTests.cs
@@ -880,7 +880,7 @@ public class RolesTests(ITestOutputHelper testOutputHelper) : UnitTestBase(testO
await s.AssertPageAccess(true, GetStorePath("invoices"));
await s.AssertPageAccess(true, GetStorePath("invoices/create"));
await s.AssertPageAccess(true, GetStorePath("payment-requests"));
- await s.AssertPageAccess(true, GetStorePath("payment-requests/edit"));
+ await s.AssertPageAccess(true, GetStorePath("payment-requests/new"));
await s.AssertPageAccess(true, GetStorePath("pull-payments"));
await s.AssertPageAccess(true, GetStorePath("payouts"));
await s.AssertPageAccess(true, GetStorePath("onchain/BTC"));
@@ -915,7 +915,7 @@ public class RolesTests(ITestOutputHelper testOutputHelper) : UnitTestBase(testO
await s.AssertPageAccess(true, GetStorePath("invoices"));
await s.AssertPageAccess(true, GetStorePath("invoices/create"));
await s.AssertPageAccess(true, GetStorePath("payment-requests"));
- await s.AssertPageAccess(true, GetStorePath("payment-requests/edit"));
+ await s.AssertPageAccess(true, GetStorePath("payment-requests/new"));
await s.AssertPageAccess(true, GetStorePath("pull-payments"));
await s.AssertPageAccess(true, GetStorePath("payouts"));
await s.AssertPageAccess(false, GetStorePath("onchain/BTC"));
@@ -942,7 +942,7 @@ public class RolesTests(ITestOutputHelper testOutputHelper) : UnitTestBase(testO
await s.AssertPageAccess(true, GetStorePath("invoices"));
await s.AssertPageAccess(true, GetStorePath("invoices/create"));
await s.AssertPageAccess(true, GetStorePath("payment-requests"));
- await s.AssertPageAccess(true, GetStorePath("payment-requests/edit"));
+ await s.AssertPageAccess(true, GetStorePath("payment-requests/new"));
await s.AssertPageAccess(true, GetStorePath("pull-payments"));
await s.AssertPageAccess(true, GetStorePath("payouts"));
await s.AssertPageAccess(false, GetStorePath("onchain/BTC"));
@@ -969,7 +969,7 @@ public class RolesTests(ITestOutputHelper testOutputHelper) : UnitTestBase(testO
await s.AssertPageAccess(true, GetStorePath("invoices"));
await s.AssertPageAccess(true, GetStorePath("invoices/create"));
await s.AssertPageAccess(true, GetStorePath("payment-requests"));
- await s.AssertPageAccess(false, GetStorePath("payment-requests/edit"));
+ await s.AssertPageAccess(false, GetStorePath("payment-requests/new"));
await s.AssertPageAccess(true, GetStorePath("pull-payments"));
await s.AssertPageAccess(true, GetStorePath("payouts"));
await s.AssertPageAccess(false, GetStorePath("onchain/BTC"));
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldPaymentRequestsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldPaymentRequestsController.cs
index 006f3cd..266400a 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldPaymentRequestsController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldPaymentRequestsController.cs
@@ -196,7 +196,7 @@ namespace BTCPayServer.Controllers.Greenfield
var storeData = HttpContext.GetStoreData();
pr = new PaymentRequestData()
{
- StoreDataId = storeId,
+ StoreDataId = storeData.Id,
Status = PaymentRequestStatus.Pending,
Created = DateTimeOffset.UtcNow,
Amount = request.Amount,
@@ -236,7 +236,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
CreatedTime = data.Created,
Id = data.Id,
- StoreId = data.StoreDataId,
+ StoreId = data.StoreDataId,
Status = data.Status,
Archived = data.Archived,
Amount = data.Amount,
diff --git a/BTCPayServer/Controllers/UIAppsController.Dashboard.cs b/BTCPayServer/Controllers/UIAppsController.Dashboard.cs
index a139f64..495b2ab 100644
--- a/BTCPayServer/Controllers/UIAppsController.Dashboard.cs
+++ b/BTCPayServer/Controllers/UIAppsController.Dashboard.cs
@@ -51,7 +51,7 @@ namespace BTCPayServer.Controllers
AppSalesPeriod.Month => 30,
_ => throw new ArgumentException($"AppSalesPeriod {period} does not exist.")
};
- var stats = await _appService.GetSalesStats(app, days);
+ var stats = await appService.GetSalesStats(app, days);
return Json(stats);
}
}
diff --git a/BTCPayServer/Controllers/UIAppsController.cs b/BTCPayServer/Controllers/UIAppsController.cs
index d826bca..96be228 100644
--- a/BTCPayServer/Controllers/UIAppsController.cs
+++ b/BTCPayServer/Controllers/UIAppsController.cs
@@ -22,38 +22,21 @@ using Microsoft.Extensions.Localization;
namespace BTCPayServer.Controllers
{
[Route("apps")]
- public partial class UIAppsController : Controller
+ public partial class UIAppsController(
+ PaymentMethodHandlerDictionary handlers,
+ BTCPayNetworkProvider networkProvider,
+ StoreRepository storeRepository,
+ IFileService fileService,
+ AppService appService,
+ IStringLocalizer stringLocalizer,
+ ViewLocalizer viewLocalizer,
+ IHtmlHelper html)
+ : Controller
{
- public UIAppsController(
- PaymentMethodHandlerDictionary handlers,
- BTCPayNetworkProvider networkProvider,
- StoreRepository storeRepository,
- IFileService fileService,
- AppService appService,
- IStringLocalizer stringLocalizer,
- ViewLocalizer viewLocalizer,
- IHtmlHelper html)
- {
- _handlers = handlers;
- _networkProvider = networkProvider;
- _storeRepository = storeRepository;
- _fileService = fileService;
- _appService = appService;
- Html = html;
- StringLocalizer = stringLocalizer;
- ViewLocalizer = viewLocalizer;
- }
-
- private readonly PaymentMethodHandlerDictionary _handlers;
- private readonly BTCPayNetworkProvider _networkProvider;
- private readonly StoreRepository _storeRepository;
- private readonly IFileService _fileService;
- private readonly AppService _appService;
-
public string CreatedAppId { get; set; }
- public IHtmlHelper Html { get; }
- public IStringLocalizer StringLocalizer { get; }
- public ViewLocalizer ViewLocalizer { get; }
+ public IHtmlHelper Html { get; } = html;
+ public IStringLocalizer StringLocalizer { get; } = stringLocalizer;
+ public ViewLocalizer ViewLocalizer { get; } = viewLocalizer;
public class AppUpdated
{
@@ -65,11 +48,11 @@ namespace BTCPayServer.Controllers
[HttpGet("/apps/{appId}")]
public async Task<IActionResult> RedirectToApp(string appId)
{
- var app = await _appService.GetApp(appId, null);
+ var app = await appService.GetApp(appId, null);
if (app is null)
return NotFound();
- var res = await _appService.ViewLink(app);
+ var res = await appService.ViewLink(app);
if (res is null)
{
return NotFound();
@@ -88,7 +71,7 @@ namespace BTCPayServer.Controllers
)
{
var store = HttpContext.GetStoreData();
- var apps = (await _appService.GetAllApps(GetUserId(), false, store.Id, archived))
+ var apps = (await appService.GetAllApps(GetUserId(), false, store.Id, archived))
.Where(app => app.Archived == archived);
if (sortOrder != null && sortOrderColumn != null)
@@ -126,7 +109,7 @@ namespace BTCPayServer.Controllers
[HttpGet("/stores/{storeId}/apps/create/{appType?}")]
public IActionResult CreateApp(string storeId, string appType = null)
{
- var vm = new CreateAppViewModel(_appService)
+ var vm = new CreateAppViewModel(appService)
{
StoreId = storeId,
AppType = appType,
@@ -140,9 +123,9 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> CreateApp(string storeId, CreateAppViewModel vm)
{
var store = HttpContext.GetStoreData();
- if (!store.AnyPaymentMethodAvailable(_handlers))
+ if (!store.AnyPaymentMethodAvailable(handlers))
{
- object text = _networkProvider.DefaultNetwork?.CryptoCode switch
+ object text = networkProvider.DefaultNetwork?.CryptoCode switch
{
null => StringLocalizer["To create a {0} app, you need to set up a wallet first", vm.AppType],
{} cryptoCode => ViewLocalizer["To create a {0} app, you need to <a href='{1}' class='alert-link'>set up a wallet</a> first", vm.AppType, Url.Action(nameof(UIStoreOnChainWalletsController.SetupWallet), "UIStoreOnChainWallets", new { area = WalletsPlugin.Area, cryptoCode, storeId })!]
@@ -157,7 +140,7 @@ namespace BTCPayServer.Controllers
return View(vm);
}
vm.StoreId = store.Id;
- var type = _appService.GetAppType(vm.AppType ?? vm.SelectedAppType);
+ var type = appService.GetAppType(vm.AppType ?? vm.SelectedAppType);
if (type is null)
{
ModelState.AddModelError(nameof(vm.SelectedAppType), StringLocalizer["Invalid App Type"]);
@@ -176,8 +159,8 @@ namespace BTCPayServer.Controllers
};
var defaultCurrency = await GetStoreDefaultCurrentIfEmpty(appData.StoreDataId, null);
- await _appService.SetDefaultSettings(appData, defaultCurrency);
- await _appService.UpdateOrCreateApp(appData);
+ await appService.SetDefaultSettings(appData, defaultCurrency);
+ await appService.UpdateOrCreateApp(appData);
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["App successfully created"].Value;
CreatedAppId = appData.Id;
@@ -205,7 +188,7 @@ namespace BTCPayServer.Controllers
if (app == null)
return NotFound();
- if (await _appService.DeleteApp(app))
+ if (await appService.DeleteApp(app))
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["App deleted successfully."].Value;
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId = app.StoreDataId });
@@ -219,14 +202,14 @@ namespace BTCPayServer.Controllers
if (app == null)
return NotFound();
- var type = _appService.GetAppType(app.AppType);
+ var type = appService.GetAppType(app.AppType);
if (type is null)
{
return UnprocessableEntity();
}
var archived = !app.Archived;
- if (await _appService.SetArchived(app, archived))
+ if (await appService.SetArchived(app, archived))
{
TempData[WellKnownTempData.SuccessMessage] = archived
? StringLocalizer["The app has been archived and will no longer appear in the apps list by default."].Value
@@ -272,9 +255,9 @@ namespace BTCPayServer.Controllers
}
try
{
- var storedFile = await _fileService.AddFile(file, userId);
+ var storedFile = await fileService.AddFile(file, userId);
var fileId = storedFile.Id;
- var fileUrl = await _fileService.GetFileUrl(Request.GetAbsoluteRootUri(), fileId);
+ var fileUrl = await fileService.GetFileUrl(Request.GetAbsoluteRootUri(), fileId);
return Json(new { fileId, fileUrl });
}
catch (Exception e)
@@ -287,7 +270,7 @@ namespace BTCPayServer.Controllers
{
if (string.IsNullOrWhiteSpace(currency))
{
- var store = await _storeRepository.FindStore(storeId);
+ var store = await storeRepository.FindStore(storeId);
currency = store?.GetStoreBlob().DefaultCurrency;
}
return currency?.Trim().ToUpperInvariant();
diff --git a/BTCPayServer/Controllers/UIInvoiceController.UI.cs b/BTCPayServer/Controllers/UIInvoiceController.UI.cs
index a6716e0..f48bae0 100644
--- a/BTCPayServer/Controllers/UIInvoiceController.UI.cs
+++ b/BTCPayServer/Controllers/UIInvoiceController.UI.cs
@@ -72,12 +72,12 @@ namespace BTCPayServer.Controllers
[HttpPost("invoices/{invoiceId}/deliveries/{deliveryId}/redeliver")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
- public async Task<IActionResult> RedeliverWebhook(string storeId, string invoiceId, string deliveryId)
+ public async Task<IActionResult> RedeliverWebhook(string invoiceId, string deliveryId)
{
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery
{
InvoiceId = [invoiceId],
- StoreId = [storeId],
+ StoreId = [this.HttpContext.GetStoreData().Id],
UserId = GetUserIdForInvoiceQuery()
})).FirstOrDefault();
if (invoice is null)
diff --git a/BTCPayServer/Controllers/UIInvoiceController.cs b/BTCPayServer/Controllers/UIInvoiceController.cs
index 72ed7cd..9ddcf11 100644
--- a/BTCPayServer/Controllers/UIInvoiceController.cs
+++ b/BTCPayServer/Controllers/UIInvoiceController.cs
@@ -22,7 +22,6 @@ using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Newtonsoft.Json.Linq;
@@ -50,7 +49,6 @@ namespace BTCPayServer.Controllers
private readonly PullPaymentHostedService _paymentHostedService;
private readonly LanguageService _languageService;
private readonly ExplorerClientProvider _ExplorerClients;
- private readonly UIWalletsController _walletsController;
private readonly InvoiceActivator _invoiceActivator;
private readonly LinkGenerator _linkGenerator;
private readonly IAuthorizationService _authorizationService;
@@ -81,7 +79,6 @@ namespace BTCPayServer.Controllers
WebhookSender webhookNotificationManager,
LanguageService languageService,
ExplorerClientProvider explorerClients,
- UIWalletsController walletsController,
InvoiceActivator invoiceActivator,
LinkGenerator linkGenerator,
AppService appService,
@@ -110,7 +107,6 @@ namespace BTCPayServer.Controllers
WebhookNotificationManager = webhookNotificationManager;
_languageService = languageService;
this._ExplorerClients = explorerClients;
- _walletsController = walletsController;
_invoiceActivator = invoiceActivator;
_linkGenerator = linkGenerator;
_authorizationService = authorizationService;
diff --git a/BTCPayServer/Controllers/UIPaymentRequestController.cs b/BTCPayServer/Controllers/UIPaymentRequestController.cs
index abbad9b..cd87ba2 100644
--- a/BTCPayServer/Controllers/UIPaymentRequestController.cs
+++ b/BTCPayServer/Controllers/UIPaymentRequestController.cs
@@ -10,6 +10,7 @@ using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
+using BTCPayServer.Events;
using BTCPayServer.Filters;
using BTCPayServer.Forms;
using BTCPayServer.Forms.Models;
@@ -24,6 +25,7 @@ using BTCPayServer.Services.Labels;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
+using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;
@@ -96,7 +98,6 @@ namespace BTCPayServer.Controllers
{
model = this.ParseListQuery(model ?? new ListPaymentRequestsViewModel());
- var store = GetCurrentStore();
var timezoneOffset = model.TimezoneOffset ?? 0;
var fs = new SearchString(model.SearchTerm, timezoneOffset);
var textSearch = model.SearchText;
@@ -106,7 +107,7 @@ namespace BTCPayServer.Controllers
var result = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery
{
UserId = GetUserId(),
- StoreId = store.Id,
+ StoreId = storeId,
Skip = model.Skip,
Count = model.Count,
Status = fs.GetFilterArray("status")?.Select(s => Enum.Parse<PaymentRequestStatus>(s, true)).ToArray(),
@@ -127,7 +128,7 @@ namespace BTCPayServer.Controllers
var paymentRequestIds = items.Select(i => i.Id).ToArray();
var labelsByPaymentRequestId =
- await _storeLabelRepository.GetStoreLabelsForObjects(store.Id, WalletObjectData.Types.PaymentRequest, paymentRequestIds);
+ await _storeLabelRepository.GetStoreLabelsForObjects(storeId, WalletObjectData.Types.PaymentRequest, paymentRequestIds);
foreach (var item in items)
{
@@ -146,7 +147,7 @@ namespace BTCPayServer.Controllers
}
}
- var allLabels = await _storeLabelRepository.GetStoreLabels(store.Id, WalletObjectData.Types.PaymentRequest);
+ var allLabels = await _storeLabelRepository.GetStoreLabels(storeId, WalletObjectData.Types.PaymentRequest);
model.Labels = allLabels
.Select(l => new TransactionTagModel
{
@@ -161,50 +162,69 @@ namespace BTCPayServer.Controllers
return View(model);
}
- [HttpGet("/stores/{storeId}/payment-requests/edit/{payReqId?}")]
+ [HttpGet("{payReqId}/edit")]
+ [HttpGet("/stores/{storeId}/payment-requests/new")]
[Authorize(Policy = Policies.CanModifyPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
- public async Task<IActionResult> EditPaymentRequest(string storeId, string payReqId)
+ public async Task<IActionResult> EditPaymentRequest(string payReqId, string clonedPayReqId = null)
{
- var store = GetCurrentStore();
- if (store == null)
+ var isNew = payReqId is null;
+ var store = HttpContext.GetStoreData();
+ PaymentRequestData paymentRequest = null;
+
+ if ((clonedPayReqId, payReqId) is (not null, null))
{
- return NotFound();
+ paymentRequest = await _PaymentRequestRepository.FindPaymentRequest(clonedPayReqId, User.GetId());
+ if (paymentRequest is null)
+ return NotFound();
}
-
- var paymentRequest = GetCurrentPaymentRequest();
- if (paymentRequest == null && !string.IsNullOrEmpty(payReqId))
+ else if ((clonedPayReqId, payReqId) is (null, not null))
+ {
+ paymentRequest = GetCurrentPaymentRequest(payReqId);
+ if (paymentRequest is null)
+ return NotFound();
+ }
+ else if ((clonedPayReqId, payReqId) is (not null, not null))
{
return NotFound();
}
if (!store.AnyPaymentMethodAvailable(_handlers))
{
- return NoPaymentMethodResult(storeId);
+ return NoPaymentMethodResult(store.Id);
}
-
var storeBlob = store.GetStoreBlob();
- var prInvoices = payReqId is null ? null : (await _PaymentRequestService.GetPaymentRequest(payReqId, GetUserId())).Invoices;
var vm = new UpdatePaymentRequestViewModel(paymentRequest)
{
- StoreId = store.Id,
- AmountAndCurrencyEditable = payReqId is null || !prInvoices.Any()
+ AmountAndCurrencyEditable = isNew || !await HasInvoice(payReqId)
};
+ if (isNew && clonedPayReqId is not null && paymentRequest is not null)
+ {
+ vm.Archived = false;
+ vm.ExpiryDate = null;
+ vm.Title = $"Clone of {vm.Title}";
+ }
vm.Currency ??= storeBlob.DefaultCurrency;
vm.HasEmailRules = await HasEmailRules(store.Id);
- if (string.IsNullOrEmpty(payReqId))
- return View(nameof(EditPaymentRequest), vm);
-
- var labels = await _storeLabelRepository.GetStoreLabelsForObjects(store.Id, WalletObjectData.Types.PaymentRequest, new[] { payReqId });
- if (labels.TryGetValue(payReqId, out var labelTuples))
+ if (paymentRequest is not null)
{
- vm.Labels = labelTuples.Select(l => l.Label).ToList();
+ var labels = await _storeLabelRepository.GetStoreLabelsForObjects(store.Id, WalletObjectData.Types.PaymentRequest, new[] { paymentRequest.Id });
+ if (labels.TryGetValue(paymentRequest.Id, out var labelTuples))
+ {
+ vm.Labels = labelTuples.Select(l => l.Label).ToList();
+ }
}
return View(nameof(EditPaymentRequest), vm);
}
+ private async Task<bool> HasInvoice(string payReqId)
+ {
+ var prInvoices = payReqId is null ? null : (await _PaymentRequestService.GetPaymentRequest(payReqId, GetUserId())).Invoices;
+ return prInvoices is not null && prInvoices.Any();
+ }
+
private async Task<bool> HasEmailRules(string storeId)
{
await using var ctx = _dbContextFactory.CreateContext();
@@ -213,11 +233,14 @@ namespace BTCPayServer.Controllers
.AnyAsync(r => r.StoreId == storeId && EF.Functions.Like(r.Trigger, "WH-PaymentRequest%"));
}
- [HttpPost("/stores/{storeId}/payment-requests/edit/{payReqId?}")]
+ [HttpPost("{payReqId}/edit")]
+ [HttpPost("/stores/{storeId}/payment-requests/new")]
[Authorize(Policy = Policies.CanModifyPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> EditPaymentRequest(string payReqId, UpdatePaymentRequestViewModel viewModel)
{
- viewModel.Id = payReqId;
+ var store = HttpContext.GetStoreData();
+ var paymentRequest = GetCurrentPaymentRequest(payReqId);
+
if (!string.IsNullOrEmpty(viewModel.Currency) &&
_Currencies.GetCurrencyData(viewModel.Currency, false) == null)
ModelState.AddModelError(nameof(viewModel.Currency), "Invalid currency");
@@ -225,15 +248,6 @@ namespace BTCPayServer.Controllers
if (string.IsNullOrEmpty(viewModel.Currency))
viewModel.Currency = null;
- var store = GetCurrentStore();
- var paymentRequest = GetCurrentPaymentRequest();
-
- viewModel.StoreId = store.Id;
-
- if ((paymentRequest == null && !string.IsNullOrEmpty(payReqId)) ||
- (paymentRequest != null && paymentRequest.Id != payReqId))
- return NotFound();
-
if (!store.AnyPaymentMethodAvailable(_handlers))
return NoPaymentMethodResult(store.Id);
@@ -266,7 +280,6 @@ namespace BTCPayServer.Controllers
var data = paymentRequest ?? new PaymentRequestData();
- data.StoreDataId = store.Id;
data.Archived = viewModel.Archived;
var blob = data.GetBlob();
@@ -280,6 +293,7 @@ namespace BTCPayServer.Controllers
viewModel.Currency = data.Currency;
}
+ data.StoreDataId = store.Id;
data.Title = viewModel.Title;
blob.Email = viewModel.Email;
blob.Description = viewModel.Description;
@@ -342,14 +356,9 @@ namespace BTCPayServer.Controllers
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
public async Task<IActionResult> ViewPaymentRequestForm(string payReqId, FormViewModel viewModel)
{
- var result = await _PaymentRequestRepository.FindPaymentRequest(payReqId, GetUserId());
- if (result == null)
- {
- return NotFound();
- }
-
- var prBlob = result.GetBlob();
- if (prBlob.FormResponse is not null)
+ var result = await _PaymentRequestRepository.FindPaymentRequest(payReqId, null);
+ var prBlob = result?.GetBlob();
+ if (prBlob?.FormResponse is not null || prBlob?.FormId is null)
{
return RedirectToAction("PayPaymentRequest", new { payReqId });
}
@@ -522,38 +531,17 @@ namespace BTCPayServer.Controllers
return Ok(StringLocalizer["Payment cancelled"]);
}
- [HttpGet("{payReqId}/clone")]
- [Authorize(Policy = Policies.CanModifyPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
- public async Task<IActionResult> ClonePaymentRequest(string payReqId)
- {
- var store = GetCurrentStore();
- var result = await EditPaymentRequest(store.Id, payReqId);
- if (result is ViewResult { Model: UpdatePaymentRequestViewModel model })
- {
- model.Id = null;
- model.Archived = false;
- model.ExpiryDate = null;
- model.Title = $"Clone of {model.Title}";
- model.AmountAndCurrencyEditable = true;
- return View("EditPaymentRequest", model);
- }
-
- return NotFound();
- }
-
[HttpGet("{payReqId}/archive")]
[Authorize(Policy = Policies.CanModifyPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> TogglePaymentRequestArchival(string payReqId)
{
- var store = GetCurrentStore();
-
var result = await _PaymentRequestRepository.ArchivePaymentRequest(payReqId, true);
if (result is not null)
{
TempData[WellKnownTempData.SuccessMessage] = result.Value
? StringLocalizer["The payment request has been archived and will no longer appear in the payment request list by default again."].Value
: StringLocalizer["The payment request has been unarchived and will appear in the payment request list by default."].Value;
- return RedirectToAction("GetPaymentRequests", new { storeId = store.Id });
+ return RedirectToAction("GetPaymentRequests", new { storeId = HttpContext.GetStoreData().Id });
}
return NotFound();
@@ -563,11 +551,6 @@ namespace BTCPayServer.Controllers
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyPaymentRequests)]
public async Task<IActionResult> TogglePaymentRequestCompleted(string payReqId)
{
- if (string.IsNullOrWhiteSpace(payReqId))
- {
- return BadRequest("Invalid parameters");
- }
-
var paymentRequest = await _PaymentRequestRepository.FindPaymentRequest(payReqId, GetUserId());
if (paymentRequest == null)
{
@@ -581,7 +564,7 @@ namespace BTCPayServer.Controllers
await _PaymentRequestRepository.UpdatePaymentRequestStatus(payReqId, PaymentRequestStatus.Completed);
- return RedirectToAction("GetPaymentRequests", new { storeId = paymentRequest.StoreDataId });
+ return RedirectToAction("GetPaymentRequests", new { storeId = HttpContext.GetStoreData().Id });
}
[HttpGet("/stores/{storeId}/payment-requests/labels")]
@@ -610,10 +593,6 @@ namespace BTCPayServer.Controllers
[Authorize(Policy = Policies.CanModifyPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> DeletePaymentRequestLabel(string storeId, string id)
{
- var store = GetCurrentStore();
- if (store is null || store.Id != storeId)
- return NotFound();
-
if (WalletObjectData.Types.AllTypes.Contains(id))
{
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["This label cannot be deleted."].Value;
@@ -646,10 +625,6 @@ namespace BTCPayServer.Controllers
if (newLabel == id)
return RedirectToAction(nameof(PaymentRequestLabels), new { storeId });
- var store = GetCurrentStore();
- if (store is null || store.Id != storeId)
- return NotFound();
-
if (WalletObjectData.Types.AllTypes.Contains(id) || WalletObjectData.Types.AllTypes.Contains(newLabel))
{
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["This label cannot be renamed."].Value;
@@ -671,9 +646,10 @@ namespace BTCPayServer.Controllers
private string GetUserId() => User.GetIdOrNull();
- private StoreData GetCurrentStore() => HttpContext.GetStoreData();
-
- private PaymentRequestData GetCurrentPaymentRequest() => HttpContext.GetPaymentRequestDataOrNull();
+ private PaymentRequestData GetCurrentPaymentRequest(string payReqId = null) =>
+ HttpContext.GetPaymentRequestDataOrNull() is {} res
+ ? payReqId is null || payReqId == res.Id ? res : null
+ : null;
private IActionResult NoPaymentMethodResult(string storeId)
{
diff --git a/BTCPayServer/Controllers/UIPullPaymentController.cs b/BTCPayServer/Controllers/UIPullPaymentController.cs
index 8bf6c13..fc7b913 100644
--- a/BTCPayServer/Controllers/UIPullPaymentController.cs
+++ b/BTCPayServer/Controllers/UIPullPaymentController.cs
@@ -113,6 +113,7 @@ namespace BTCPayServer.Controllers
return registerUrl;
}
+ [HttpGet("pull-payments/edit/{pullPaymentId}")]
[HttpGet("stores/{storeId}/pull-payments/edit/{pullPaymentId}")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> EditPullPayment(string storeId, string pullPaymentId)
@@ -127,6 +128,7 @@ namespace BTCPayServer.Controllers
return View(vm);
}
+ [HttpPost("pull-payments/edit/{pullPaymentId}")]
[HttpPost("stores/{storeId}/pull-payments/edit/{pullPaymentId}")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> EditPullPayment(string storeId, string pullPaymentId, UpdatePullPaymentModel viewModel)
@@ -161,7 +163,7 @@ namespace BTCPayServer.Controllers
Severity = StatusMessageModel.StatusSeverity.Success
});
- return RedirectToAction(nameof(UIStorePullPaymentsController.PullPayments), "UIStorePullPayments", new { storeId, pullPaymentId });
+ return RedirectToAction(nameof(UIStorePullPaymentsController.PullPayments), "UIStorePullPayments", new { storeId = pp.StoreId, pullPaymentId });
}
[AllowAnonymous]
diff --git a/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs b/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs
index 2a38bdc..2f1ca56 100644
--- a/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs
+++ b/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs
@@ -80,9 +80,6 @@ namespace BTCPayServer.Controllers
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public IActionResult NewPullPayment(string storeId)
{
- if (CurrentStore is null)
- return NotFound();
-
var paymentMethods = _payoutHandlers.GetSupportedPayoutMethods(CurrentStore);
if (!paymentMethods.Any())
{
@@ -107,9 +104,6 @@ namespace BTCPayServer.Controllers
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> NewPullPayment(string storeId, NewPullPaymentModel model)
{
- if (CurrentStore is null)
- return NotFound();
-
var paymentMethodOptions = _payoutHandlers.GetSupportedPayoutMethods(CurrentStore);
model.PayoutMethodsItem =
paymentMethodOptions.Select(id => new SelectListItem(id.ToString(), id.ToString(), true));
@@ -256,19 +250,19 @@ namespace BTCPayServer.Controllers
return View(vm);
}
+ [HttpGet("pull-payments/{pullPaymentId}/archive")]
[HttpGet("stores/{storeId}/pull-payments/{pullPaymentId}/archive")]
[Authorize(Policy = Policies.CanArchivePullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
- public IActionResult ArchivePullPayment(string storeId,
- string pullPaymentId)
+ public IActionResult ArchivePullPayment(string pullPaymentId)
{
return View("Confirm",
new ConfirmModel(StringLocalizer["Archive pull payment"], StringLocalizer["Do you really want to archive the pull payment?"], StringLocalizer["Archive"]));
}
+ [HttpPost("pull-payments/{pullPaymentId}/archive")]
[HttpPost("stores/{storeId}/pull-payments/{pullPaymentId}/archive")]
[Authorize(Policy = Policies.CanArchivePullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
- public async Task<IActionResult> ArchivePullPaymentPost(string storeId,
- string pullPaymentId)
+ public async Task<IActionResult> ArchivePullPaymentPost(string pullPaymentId)
{
await _pullPaymentService.Cancel(new PullPaymentHostedService.CancelRequest(pullPaymentId));
TempData.SetStatusMessageModel(new StatusMessageModel
@@ -276,19 +270,20 @@ namespace BTCPayServer.Controllers
Message = StringLocalizer["Pull payment archived"].Value,
Severity = StatusMessageModel.StatusSeverity.Success
});
- return RedirectToAction(nameof(PullPayments), new { storeId });
+ return RedirectToAction(nameof(PullPayments), new { storeId = HttpContext.GetStoreData().Id });
}
[Authorize(Policy = Policies.CanManagePayouts, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
+ [HttpPost("pull-payments/{pullPaymentId}/payouts")]
[HttpPost("stores/{storeId}/pull-payments/payouts")]
[HttpPost("stores/{storeId}/pull-payments/{pullPaymentId}/payouts")]
[HttpPost("stores/{storeId}/payouts")]
- public async Task<IActionResult> PayoutsPost(
- string storeId, PayoutsModel vm, CancellationToken cancellationToken)
+ public async Task<IActionResult> PayoutsPost(PayoutsModel vm, CancellationToken cancellationToken)
{
if (vm is null)
return NotFound();
+ var storeId = HttpContext.GetStoreData().Id;
var store = HttpContext.GetStoreData();
vm.PayoutMethods = _payoutHandlers.GetSupportedPayoutMethods(store);
vm.HasPayoutProcessor = await HasPayoutProcessor(storeId, vm.PayoutMethodId);
@@ -494,13 +489,15 @@ namespace BTCPayServer.Controllers
}, ctx, cancellationToken);
}
+ [HttpGet("pull-payments/{pullPaymentId}/payouts")]
[HttpGet("stores/{storeId}/pull-payments/{pullPaymentId}/payouts")]
[HttpGet("stores/{storeId}/payouts")]
[Authorize(Policy = Policies.CanViewPayouts, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> Payouts(
- string storeId, string pullPaymentId, string payoutMethodId, PayoutState payoutState,
+ string pullPaymentId, string payoutMethodId, PayoutState payoutState,
int skip = 0, int count = 50)
{
+ var storeId = HttpContext.GetStoreData().Id;
var paymentMethods = _payoutHandlers.GetSupportedPayoutMethods(HttpContext.GetStoreData());
if (!paymentMethods.Any())
{
diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs
index 3d709d0..b116ab6 100644
--- a/BTCPayServer/Extensions.cs
+++ b/BTCPayServer/Extensions.cs
@@ -849,6 +849,8 @@ namespace BTCPayServer
public static PaymentRequestData? GetPaymentRequestDataOrNull(this HttpContext ctx)
=> ctx.Items.TryGet("BTCPAY.PAYMENTREQUESTDATA") as PaymentRequestData;
+ public static PaymentRequestData? GetPaymentRequestData(this HttpContext ctx)
+ => GetPaymentRequestDataOrNull(ctx) ?? throw new InvalidOperationException("BTCPAY.PAYMENTREQUESTDATA is not set");
public static void SetPaymentRequestData(this HttpContext ctx, PaymentRequestData? paymentRequestData)
{
diff --git a/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs b/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs
index 8d50f2b..9fa07d0 100644
--- a/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs
+++ b/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs
@@ -38,9 +38,6 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
{
return;
}
-
- Id = data.Id;
- StoreId = data.StoreDataId;
Archived = data.Archived;
var blob = data.GetBlob();
FormId = blob.FormId;
@@ -62,9 +59,6 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
public bool Archived { get; set; }
- public string Id { get; set; }
- [Required] public string StoreId { get; set; }
-
[Required]
[Range(double.Epsilon, double.PositiveInfinity, ErrorMessage = "Please provide an amount greater than 0")]
public decimal Amount { get; set; }
diff --git a/BTCPayServer/Security/BuiltInPermissionScopeProvider.cs b/BTCPayServer/Security/BuiltInPermissionScopeProvider.cs
index 97b3bce..e36d9a9 100644
--- a/BTCPayServer/Security/BuiltInPermissionScopeProvider.cs
+++ b/BTCPayServer/Security/BuiltInPermissionScopeProvider.cs
@@ -97,13 +97,23 @@ public class BuiltInPermissionScopeProvider(
return id2;
});
}
+ // If the route is "pull-payments/DO-NOT-EXIT/edit", this will returns 403
+ // For legacy, if the route is "stores/{storeId}/pull-payments/DO-NOT-EXIT/edit", the action itself needs to handle 404.
+ else if (storeId is null)
+ {
+ authContext.Fail();
+ return null;
+ }
}
storeId ??= storeId2;
// Consider the route /stores/{storeId}/apps/{appId}
// This check is making sure that the `storeId` is matching the scope resolved from `appId`.
if (storeId2 != storeId)
- storeId2 = null;
+ {
+ authContext.Fail();
+ return null;
+ }
if (storeId2 is not null)
additionalScopes.Add(new AdditionalScope(i.RouteValue, id));
diff --git a/BTCPayServer/Security/PermissionAuthorizationHandler.cs b/BTCPayServer/Security/PermissionAuthorizationHandler.cs
index a28a8dd..173c791 100644
--- a/BTCPayServer/Security/PermissionAuthorizationHandler.cs
+++ b/BTCPayServer/Security/PermissionAuthorizationHandler.cs
@@ -35,6 +35,8 @@ public class PermissionAuthorizationHandler(
if (!explicitScope)
scope = await GetImplicitScope(userId, context, requirement, httpContext.HttpContext);
}
+ if (context.HasFailed)
+ return;
await Handle(context, requirement, scope, userId, explicitScope, httpContext.HttpContext);
}
@@ -74,7 +76,7 @@ public class PermissionAuthorizationHandler(
foreach (var implicitScopeProvider in implicitScopeProviders)
{
var scope = await implicitScopeProvider.GetScope(context, ctx);
- if (scope is not null)
+ if (context.HasFailed || scope is not null)
return scope;
}
return null;
diff --git a/BTCPayServer/Views/UIPaymentRequest/EditPaymentRequest.cshtml b/BTCPayServer/Views/UIPaymentRequest/EditPaymentRequest.cshtml
index 6e92805..c965a2a 100644
--- a/BTCPayServer/Views/UIPaymentRequest/EditPaymentRequest.cshtml
+++ b/BTCPayServer/Views/UIPaymentRequest/EditPaymentRequest.cshtml
@@ -6,8 +6,11 @@
@inject LinkGenerator LinkGenerator
@model BTCPayServer.Models.PaymentRequestViewModels.UpdatePaymentRequestViewModel
@{
- var checkoutFormOptions = await FormDataService.GetSelect(Model.StoreId, Model.FormId);
- ViewData.SetLayoutModel(new LayoutModel("PaymentRequests", string.IsNullOrEmpty(Model.Id) ? StringLocalizer["Create Payment Request"] : StringLocalizer["Edit Payment Request"]));
+ var storeId = this.Context.GetStoreData().Id;
+ var payReq = this.Context.GetPaymentRequestDataOrNull();
+ var payreqId = payReq?.Id;
+ var checkoutFormOptions = await FormDataService.GetSelect(storeId, Model.FormId);
+ ViewData.SetLayoutModel(new LayoutModel("PaymentRequests", payreqId is null ? StringLocalizer["Create Payment Request"] : StringLocalizer["Edit Payment Request"]));
}
@section PageHeadContent {
@@ -21,26 +24,26 @@
<script src="~/vendor/summernote/summernote-bs5.js" asp-append-version="true"></script>
}
-<form method="post" action="@Url.Action("EditPaymentRequest", "UIPaymentRequest", new { storeId = Model.StoreId, payReqId = Model.Id }, Context.Request.Scheme)">
+<form method="post">
<div class="sticky-header">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
- <a asp-action="GetPaymentRequests" asp-route-storeId="@Model.StoreId" text-translate="true">Payment Requests</a>
+ <a asp-action="GetPaymentRequests" asp-route-storeId="@storeId" text-translate="true">Payment Requests</a>
</li>
<li class="breadcrumb-item active" aria-current="page">@ViewData["Title"]</li>
</ol>
<h2>@ViewData["Title"]</h2>
</nav>
<div>
- @if (string.IsNullOrEmpty(Model.Id))
+ @if (payreqId is null)
{
<button id="page-primary" type="submit" class="btn btn-primary" permission="@Policies.CanModifyPaymentRequests" text-translate="true">Create</button>
}
else
{
<button id="page-primary" type="submit" class="btn btn-primary order-sm-1" permission="@Policies.CanModifyPaymentRequests" text-translate="true">Save</button>
- <a class="btn btn-secondary" asp-action="ViewPaymentRequest" asp-route-payReqId="@Model.Id" id="ViewPaymentRequest" target="_blank" text-translate="true">View</a>
+ <a class="btn btn-secondary" asp-action="ViewPaymentRequest" asp-route-payReqId="@payreqId" id="ViewPaymentRequest" target="_blank" text-translate="true">View</a>
}
</div>
</div>
@@ -110,9 +113,9 @@
select-element="Labels"
auto-update="false"
linked-type="@WalletObjectData.Types.PaymentRequest"
- store-id="@Model.StoreId"
- store-object-id="@Model.Id" />
- <a asp-action="PaymentRequestLabels" asp-controller="UIPaymentRequest" asp-route-storeId="@Model.StoreId"
+ store-id="@storeId"
+ store-object-id="@payreqId" />
+ <a asp-action="PaymentRequestLabels" asp-controller="UIPaymentRequest" asp-route-storeId="@storeId"
class="btn btn-secondary input-group-clear"
title="@StringLocalizer["Manage Labels"]">
<vc:icon symbol="settings" />
@@ -137,7 +140,7 @@
<span asp-validation-for="Email" class="text-danger"></span>
<div id="PaymentRequestEmailHelpBlock" class="form-text">
@ViewLocalizer["This will send notification mails to the recipient, as configured by the <a href=\"{0}\">email rules</a>.",
- LinkGenerator.GetStoreEmailRulesLink(Model.StoreId, Context.Request.GetRequestBaseUrl())]
+ LinkGenerator.GetStoreEmailRulesLink(storeId, Context.Request.GetRequestBaseUrl())]
@if (Model.HasEmailRules is not true)
{
<div class="info-note mt-1 text-warning" role="alert">
@@ -173,23 +176,23 @@
</div>
</form>
-@if (!string.IsNullOrEmpty(Model.Id))
+@if (payreqId is not null)
{
<div class="d-flex gap-3 mt-3">
<a class="btn btn-secondary"
permission="@Policies.CanViewInvoices"
asp-action="ListInvoices"
asp-controller="UIInvoice"
- asp-route-storeId="@Model.StoreId"
- asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(Model.Id)}")" text-translate="true">Invoices</a>
- <a class="btn btn-secondary" asp-route-payReqId="@Model.Id" asp-action="ClonePaymentRequest" id="ClonePaymentRequest" permission="@Policies.CanModifyPaymentRequests" text-translate="true">Clone</a>
+ asp-route-storeId="@storeId"
+ asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(payreqId)}")" text-translate="true">Invoices</a>
+ <a class="btn btn-secondary" asp-route-storeId="@storeId" asp-route-payReqId="" asp-route-clonedPayReqId="@payreqId" asp-action="EditPaymentRequest" id="ClonePaymentRequest" permission="@Policies.CanModifyPaymentRequests" text-translate="true">Clone</a>
@if (!Model.Archived)
{
- <a class="btn btn-secondary" data-bs-toggle="tooltip" title="@StringLocalizer["Archive this payment request so that it does not appear in the payment request list by default"]" asp-controller="UIPaymentRequest" asp-action="TogglePaymentRequestArchival" asp-route-payReqId="@Model.Id" id="ArchivePaymentRequest" permission="@Policies.CanModifyPaymentRequests" text-translate="true">Archive</a>
+ <a class="btn btn-secondary" data-bs-toggle="tooltip" title="@StringLocalizer["Archive this payment request so that it does not appear in the payment request list by default"]" asp-controller="UIPaymentRequest" asp-action="TogglePaymentRequestArchival" asp-route-payReqId="@payreqId" id="ArchivePaymentRequest" permission="@Policies.CanModifyPaymentRequests" text-translate="true">Archive</a>
}
else
{
- <a class="btn btn-secondary" data-bs-toggle="tooltip" title="@StringLocalizer["Unarchive this payment request"]" asp-controller="UIPaymentRequest" asp-action="TogglePaymentRequestArchival" asp-route-payReqId="@Model.Id" id="UnarchivePaymentRequest" permission="@Policies.CanModifyPaymentRequests" text-translate="true">Unarchive</a>
+ <a class="btn btn-secondary" data-bs-toggle="tooltip" title="@StringLocalizer["Unarchive this payment request"]" asp-controller="UIPaymentRequest" asp-action="TogglePaymentRequestArchival" asp-route-payReqId="@payreqId" id="UnarchivePaymentRequest" permission="@Policies.CanModifyPaymentRequests" text-translate="true">Unarchive</a>
}
</div>
}
diff --git a/BTCPayServer/Views/UIPaymentRequest/GetPaymentRequests.cshtml b/BTCPayServer/Views/UIPaymentRequest/GetPaymentRequests.cshtml
index 4e47b61..d40d6b8 100644
--- a/BTCPayServer/Views/UIPaymentRequest/GetPaymentRequests.cshtml
+++ b/BTCPayServer/Views/UIPaymentRequest/GetPaymentRequests.cshtml
@@ -315,7 +315,7 @@
@item.Created.ToBrowserDate()
</td>
<td>
- <a asp-action="EditPaymentRequest" asp-route-storeId="@item.StoreId" asp-route-payReqId="@item.Id" id="Edit-@item.Id">@item.Title</a>
+ <a asp-action="EditPaymentRequest" asp-route-payReqId="@item.Id" id="Edit-@item.Id">@item.Title</a>
@if (item.ExpiryDate.HasValue && item.Status != "Expired")
{
<div><span class="text-muted small">Expires: @item.ExpiryDate?.ToBrowserDate()</span></div>
@@ -360,9 +360,9 @@
</button>
<ul class="dropdown-menu" aria-labelledby="actionDropdown">
<li><a class="dropdown-item" permission="@Policies.CanViewInvoices" asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@item.StoreId" asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(item.Id)}")" text-translate="true">Invoices</a></li>
- <li><a class="dropdown-item" permission="@Policies.CanModifyPaymentRequests" asp-action="ClonePaymentRequest" asp-route-storeId="@item.StoreId" asp-route-payReqId="@item.Id" id="Clone-@item.Id" text-translate="true">Clone</a></li>
+ <li><a class="dropdown-item" permission="@Policies.CanModifyPaymentRequests" asp-action="EditPaymentRequest" asp-route-storeId="@item.StoreId" asp-route-clonedPayReqId="@item.Id" id="Clone-@item.Id" text-translate="true">Clone</a></li>
<li class="dropdown-divider"></li>
- <li><a class="dropdown-item" permission="@Policies.CanModifyPaymentRequests" asp-action="TogglePaymentRequestArchival" asp-route-storeId="@item.StoreId" asp-route-payReqId="@item.Id" id="ToggleArchival-@item.Id">@(item.Archived ? "Unarchive" : "Archive")</a></li>
+ <li><a class="dropdown-item" permission="@Policies.CanModifyPaymentRequests" asp-action="TogglePaymentRequestArchival" asp-route-payReqId="@item.Id" id="ToggleArchival-@item.Id">@(item.Archived ? "Unarchive" : "Archive")</a></li>
</ul>
</div>
</td>
diff --git a/BTCPayServer/Views/UIPaymentRequest/ViewPaymentRequest.cshtml b/BTCPayServer/Views/UIPaymentRequest/ViewPaymentRequest.cshtml
index e2cb1e3..d130420 100644
--- a/BTCPayServer/Views/UIPaymentRequest/ViewPaymentRequest.cshtml
+++ b/BTCPayServer/Views/UIPaymentRequest/ViewPaymentRequest.cshtml
@@ -317,7 +317,7 @@
</main>
<footer class="store-footer">
<div permission="@Policies.CanModifyStoreSettings" class="d-print-none">
- <form asp-action="EditPaymentRequest" asp-route-storeId="@Model.StoreId" asp-route-payReqId="@Model.Id" method="get">
+ <form asp-action="EditPaymentRequest" asp-route-payReqId="@Model.Id" method="get">
<button type="submit" class="btn btn-link" text-translate="true">Edit payment request</button>
</form>
</div>
diff --git a/BTCPayServer/Views/UIPullPayment/EditPullPayment.cshtml b/BTCPayServer/Views/UIPullPayment/EditPullPayment.cshtml
index 296ba00..37a7433 100644
--- a/BTCPayServer/Views/UIPullPayment/EditPullPayment.cshtml
+++ b/BTCPayServer/Views/UIPullPayment/EditPullPayment.cshtml
@@ -14,7 +14,7 @@
<script src="~/vendor/summernote/summernote-bs5.js" asp-append-version="true"></script>
}
-<form method="post" asp-action="EditPullPayment" asp-route-storeId="@storeId" asp-route-pullPaymentId="@Model.Id">
+<form method="post" asp-action="EditPullPayment" asp-route-pullPaymentId="@Model.Id">
<div class="sticky-header">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
diff --git a/BTCPayServer/Views/UIPullPayment/ViewPullPayment.cshtml b/BTCPayServer/Views/UIPullPayment/ViewPullPayment.cshtml
index 7ea8d50..a6ba46c 100644
--- a/BTCPayServer/Views/UIPullPayment/ViewPullPayment.cshtml
+++ b/BTCPayServer/Views/UIPullPayment/ViewPullPayment.cshtml
@@ -201,7 +201,7 @@
</main>
<footer class="store-footer">
<p permission="@Policies.CanViewStoreSettings">
- <a asp-action="EditPullPayment" asp-controller="UIPullPayment" asp-route-storeId="@Model.StoreId" asp-route-pullPaymentId="@Model.Id" text-translate="true">
+ <a asp-action="EditPullPayment" asp-controller="UIPullPayment" asp-route-pullPaymentId="@Model.Id" text-translate="true">
Edit pull payment
</a>
</p>
diff --git a/BTCPayServer/Views/UIStorePullPayments/Payouts.cshtml b/BTCPayServer/Views/UIStorePullPayments/Payouts.cshtml
index ff0769d..4ef0bdd 100644
--- a/BTCPayServer/Views/UIStorePullPayments/Payouts.cshtml
+++ b/BTCPayServer/Views/UIStorePullPayments/Payouts.cshtml
@@ -6,7 +6,7 @@
@inject PayoutMethodHandlerDictionary PayoutHandlers;
@{
- var storeId = Context.GetRouteValue("storeId") as string;
+ var storeId = Context.GetStoreData().Id;
ViewData.SetLayoutModel(new(nameof(StoreNavPages.Payouts), string.IsNullOrEmpty(Model.PullPaymentName) ? StringLocalizer["Payouts"] : StringLocalizer["Payouts for pull payment {0}", Model.PullPaymentName]));
Model.PaginationQuery ??= new Dictionary<string, object>();
Model.PaginationQuery.Add("pullPaymentId", Model.PullPaymentId);
@@ -102,7 +102,7 @@
@foreach (var state in Model.PayoutMethods)
{
<li class="nav-item py-0">
- <a asp-action="Payouts" asp-route-storeId="@Context.GetRouteValue("storeId")"
+ <a asp-action="Payouts" asp-route-storeId="@(Model.PullPaymentId is null ? Context.GetRouteValue("storeId") : null)"
asp-route-payoutState="@Model.PayoutState"
asp-route-payoutMethodId="@state.ToString()"
asp-route-pullPaymentId="@Model.PullPaymentId"
@@ -125,7 +125,7 @@
{
<a id="@state.Key-view"
asp-action="Payouts"
- asp-route-storeId="@Context.GetRouteValue("storeId")"
+ asp-route-storeId="@(Model.PullPaymentId is null ? Context.GetRouteValue("storeId") : null)"
asp-route-payoutState="@state.Key"
asp-route-pullPaymentId="@Model.PullPaymentId"
asp-route-payoutMethodId="@Model.PayoutMethodId"
diff --git a/BTCPayServer/Views/UIStorePullPayments/PullPayments.cshtml b/BTCPayServer/Views/UIStorePullPayments/PullPayments.cshtml
index b3bd3eb..fcd33f4 100644
--- a/BTCPayServer/Views/UIStorePullPayments/PullPayments.cshtml
+++ b/BTCPayServer/Views/UIStorePullPayments/PullPayments.cshtml
@@ -116,7 +116,6 @@
permission="@Policies.CanManagePullPayments"
asp-action="EditPullPayment"
asp-controller="UIPullPayment"
- asp-route-storeId="@storeId"
asp-route-pullPaymentId="@pp.Id">
@pp.Name
</a>
@@ -152,17 +151,15 @@
<a class="pp-payout"
permission="@Policies.CanViewPayouts"
asp-action="Payouts"
- asp-route-storeId="@storeId"
asp-route-pullPaymentId="@pp.Id"
text-translate="true">
Payouts
</a>
@if (!pp.Archived)
{
- <a asp-action="ArchivePullPayment"
- permission="@Policies.CanArchivePullPayments"
- asp-route-storeId="@storeId"
- asp-route-pullPaymentId="@pp.Id"
+ <a asp-action="ArchivePullPayment"
+ permission="@Policies.CanArchivePullPayments"
+ asp-route-pullPaymentId="@pp.Id"
data-bs-toggle="modal"
data-bs-target="#ConfirmModal"
data-description="Do you really want to archive the pull payment <strong>@Html.Encode(pp.Name)</strong>?"
diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.webhooks.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.webhooks.json
index 77d1cf4..38a2ab1 100644
--- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.webhooks.json
+++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.webhooks.json
@@ -98,8 +98,8 @@
"tags": [
"Webhooks"
],
- "summary": "Get a webhook of a store",
- "description": "View webhook of a store",
+ "summary": "Get a webhook",
+ "description": "View webhook",
"operationId": "Webhooks_GetWebhook",
"responses": {
"200": {
Why this scored 38/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.