mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-21 14:04:12 +01:00
Allow pull payments for store guests (#3128)
This commit is contained in:
parent
e113c12768
commit
fd75008499
7 changed files with 92 additions and 29 deletions
|
@ -26,7 +26,7 @@ using StoreData = BTCPayServer.Data.StoreData;
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
[Route("stores/{storeId}/pull-payments")]
|
[Route("stores/{storeId}/pull-payments")]
|
||||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||||
[AutoValidateAntiforgeryToken]
|
[AutoValidateAntiforgeryToken]
|
||||||
public class StorePullPaymentsController: Controller
|
public class StorePullPaymentsController: Controller
|
||||||
{
|
{
|
||||||
|
@ -60,6 +60,7 @@ namespace BTCPayServer.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("new")]
|
[HttpGet("new")]
|
||||||
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||||
public async Task<IActionResult> NewPullPayment(string storeId)
|
public async Task<IActionResult> NewPullPayment(string storeId)
|
||||||
{
|
{
|
||||||
if (CurrentStore is null)
|
if (CurrentStore is null)
|
||||||
|
@ -86,6 +87,7 @@ namespace BTCPayServer.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("new")]
|
[HttpPost("new")]
|
||||||
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||||
public async Task<IActionResult> NewPullPayment(string storeId, NewPullPaymentModel model)
|
public async Task<IActionResult> NewPullPayment(string storeId, NewPullPaymentModel model)
|
||||||
{
|
{
|
||||||
if (CurrentStore is null)
|
if (CurrentStore is null)
|
||||||
|
@ -217,6 +219,7 @@ namespace BTCPayServer.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{pullPaymentId}/archive")]
|
[HttpGet("{pullPaymentId}/archive")]
|
||||||
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||||
public IActionResult ArchivePullPayment(string storeId,
|
public IActionResult ArchivePullPayment(string storeId,
|
||||||
string pullPaymentId)
|
string pullPaymentId)
|
||||||
{
|
{
|
||||||
|
@ -224,6 +227,7 @@ namespace BTCPayServer.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{pullPaymentId}/archive")]
|
[HttpPost("{pullPaymentId}/archive")]
|
||||||
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||||
public async Task<IActionResult> ArchivePullPaymentPost(string storeId,
|
public async Task<IActionResult> ArchivePullPaymentPost(string storeId,
|
||||||
string pullPaymentId)
|
string pullPaymentId)
|
||||||
{
|
{
|
||||||
|
@ -236,6 +240,7 @@ namespace BTCPayServer.Controllers
|
||||||
return RedirectToAction(nameof(PullPayments), new { storeId = storeId });
|
return RedirectToAction(nameof(PullPayments), new { storeId = storeId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||||
[HttpPost("payouts")]
|
[HttpPost("payouts")]
|
||||||
public async Task<IActionResult> PayoutsPost(
|
public async Task<IActionResult> PayoutsPost(
|
||||||
string storeId, PayoutsModel vm, CancellationToken cancellationToken)
|
string storeId, PayoutsModel vm, CancellationToken cancellationToken)
|
||||||
|
|
|
@ -37,7 +37,7 @@ namespace BTCPayServer.Security
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string storeId = _HttpContext.GetImplicitStoreId();
|
string storeId = context.Resource is string s? s :_HttpContext.GetImplicitStoreId();
|
||||||
if (storeId == null)
|
if (storeId == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -47,20 +47,20 @@ namespace BTCPayServer.Security
|
||||||
|
|
||||||
|
|
||||||
var store = await _storeRepository.FindStore(storeId, userid);
|
var store = await _storeRepository.FindStore(storeId, userid);
|
||||||
if (store == null)
|
|
||||||
return;
|
|
||||||
bool success = false;
|
bool success = false;
|
||||||
switch (requirement.Policy)
|
switch (requirement.Policy)
|
||||||
{
|
{
|
||||||
case Policies.CanModifyStoreSettings:
|
case Policies.CanModifyStoreSettings:
|
||||||
if (store.Role == StoreRoles.Owner || isAdmin)
|
if (store != null && (store.Role == StoreRoles.Owner || isAdmin))
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
case Policies.CanViewStoreSettings:
|
||||||
|
if (store != null || isAdmin)
|
||||||
success = true;
|
success = true;
|
||||||
break;
|
break;
|
||||||
case Policies.CanCreateInvoice:
|
case Policies.CanCreateInvoice:
|
||||||
if (store.Role == StoreRoles.Owner ||
|
if (store != null || isAdmin)
|
||||||
store.Role == StoreRoles.Guest ||
|
|
||||||
isAdmin ||
|
|
||||||
store.GetStoreBlob().AnyoneCanInvoice)
|
|
||||||
success = true;
|
success = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
51
BTCPayServer/Security/PermissionTagHelper.cs
Normal file
51
BTCPayServer/Security/PermissionTagHelper.cs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Security
|
||||||
|
{
|
||||||
|
[HtmlTargetElement(Attributes = nameof(Permission))]
|
||||||
|
public class PermissionTagHelper : TagHelper
|
||||||
|
{
|
||||||
|
private readonly IAuthorizationService _authorizationService;
|
||||||
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
private readonly ILogger<PermissionTagHelper> _logger;
|
||||||
|
|
||||||
|
public PermissionTagHelper(IAuthorizationService authorizationService, IHttpContextAccessor httpContextAccessor, ILogger<PermissionTagHelper> logger)
|
||||||
|
{
|
||||||
|
_authorizationService = authorizationService;
|
||||||
|
_httpContextAccessor = httpContextAccessor;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Permission { get; set; }
|
||||||
|
public string PermissionResource { get; set; }
|
||||||
|
|
||||||
|
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(Permission))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = $"{Permission}_{PermissionResource}";
|
||||||
|
if (!_httpContextAccessor.HttpContext.Items.TryGetValue(key,out var cachedResult))
|
||||||
|
{
|
||||||
|
var result = await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User,
|
||||||
|
PermissionResource,
|
||||||
|
Permission);
|
||||||
|
|
||||||
|
cachedResult = result;
|
||||||
|
_httpContextAccessor.HttpContext.Items.Add(key, result);
|
||||||
|
|
||||||
|
}
|
||||||
|
if (!((AuthorizationResult)cachedResult).Succeeded)
|
||||||
|
{
|
||||||
|
output.SuppressOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@
|
||||||
@using BTCPayServer.Payments
|
@using BTCPayServer.Payments
|
||||||
@using BTCPayServer.Views.Stores
|
@using BTCPayServer.Views.Stores
|
||||||
@using BTCPayServer.Abstractions.Extensions
|
@using BTCPayServer.Abstractions.Extensions
|
||||||
|
@using BTCPayServer.Client
|
||||||
@model BTCPayServer.Models.WalletViewModels.PayoutsModel
|
@model BTCPayServer.Models.WalletViewModels.PayoutsModel
|
||||||
|
|
||||||
@inject IEnumerable<IPayoutHandler> PayoutHandlers;
|
@inject IEnumerable<IPayoutHandler> PayoutHandlers;
|
||||||
|
@ -88,9 +89,9 @@
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@if (Model.Payouts.Any() && stateActions.Any())
|
@if (Model.Payouts.Any() && stateActions.Any() )
|
||||||
{
|
{
|
||||||
<div class="col-12">
|
<div class="col-12" permission="@Policies.CanModifyStoreSettings" >
|
||||||
<div class="dropdown mt-4 ms-xl-auto mt-xl-0">
|
<div class="dropdown mt-4 ms-xl-auto mt-xl-0">
|
||||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" id="@Model.PayoutState-actions">
|
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" id="@Model.PayoutState-actions">
|
||||||
Actions
|
Actions
|
||||||
|
@ -114,7 +115,7 @@
|
||||||
<table class="table table-hover table-responsive-lg">
|
<table class="table table-hover table-responsive-lg">
|
||||||
<thead class="thead-inverse">
|
<thead class="thead-inverse">
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th permission="@Policies.CanModifyStoreSettings" >
|
||||||
<input id="@Model.PayoutState-selectAllCheckbox" type="checkbox" class="form-check-input selectAll" data-payout-state="@Model.PayoutState.ToString()" />
|
<input id="@Model.PayoutState-selectAllCheckbox" type="checkbox" class="form-check-input selectAll" data-payout-state="@Model.PayoutState.ToString()" />
|
||||||
</th>
|
</th>
|
||||||
<th style="min-width: 90px;" class="col-md-auto">
|
<th style="min-width: 90px;" class="col-md-auto">
|
||||||
|
@ -134,7 +135,7 @@
|
||||||
{
|
{
|
||||||
var pp = Model.Payouts[i];
|
var pp = Model.Payouts[i];
|
||||||
<tr class="payout">
|
<tr class="payout">
|
||||||
<td>
|
<td permission="@Policies.CanModifyStoreSettings" >
|
||||||
<span>
|
<span>
|
||||||
<input type="checkbox" class="selection-item-@Model.PayoutState.ToString() form-check-input" asp-for="Payouts[i].Selected"/>
|
<input type="checkbox" class="selection-item-@Model.PayoutState.ToString() form-check-input" asp-for="Payouts[i].Selected"/>
|
||||||
<input type="hidden" asp-for="Payouts[i].PayoutId"/>
|
<input type="hidden" asp-for="Payouts[i].PayoutId"/>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
@using BTCPayServer.Views.Stores
|
@using BTCPayServer.Views.Stores
|
||||||
@using BTCPayServer.Abstractions.Extensions
|
@using BTCPayServer.Abstractions.Extensions
|
||||||
|
@using BTCPayServer.Client
|
||||||
@model BTCPayServer.Models.WalletViewModels.PullPaymentsModel
|
@model BTCPayServer.Models.WalletViewModels.PullPaymentsModel
|
||||||
@{
|
@{
|
||||||
Layout = "../Shared/_NavLayout.cshtml";
|
Layout = "../Shared/_NavLayout.cshtml";
|
||||||
|
@ -44,7 +45,7 @@
|
||||||
</a>
|
</a>
|
||||||
</small>
|
</small>
|
||||||
</h4>
|
</h4>
|
||||||
<a asp-action="NewPullPayment" asp-route-storeId="@Context.GetRouteValue("storeId")" class="btn btn-primary" role="button" id="NewPullPayment">
|
<a permission="@Policies.CanModifyStoreSettings" asp-action="NewPullPayment" asp-route-storeId="@Context.GetRouteValue("storeId")" class="btn btn-primary" role="button" id="NewPullPayment">
|
||||||
<span class="fa fa-plus"></span> Create a new pull payment
|
<span class="fa fa-plus"></span> Create a new pull payment
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -88,7 +89,7 @@
|
||||||
</th>
|
</th>
|
||||||
<th scope="col">Name</th>
|
<th scope="col">Name</th>
|
||||||
<th scope="col">Refunded</th>
|
<th scope="col">Refunded</th>
|
||||||
<th scope="col" class="text-end">Actions</th>
|
<th scope="col" class="text-end" >Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -113,12 +114,15 @@
|
||||||
asp-route-pullPaymentId="@pp.Id">
|
asp-route-pullPaymentId="@pp.Id">
|
||||||
View
|
View
|
||||||
</a> -
|
</a> -
|
||||||
<a class="pp-payout" asp-action="Payouts"
|
<a class="pp-payout"
|
||||||
|
|
||||||
|
asp-action="Payouts"
|
||||||
asp-route-storeId="@Context.GetRouteValue("storeId")"
|
asp-route-storeId="@Context.GetRouteValue("storeId")"
|
||||||
asp-route-pullPaymentId="@pp.Id">
|
asp-route-pullPaymentId="@pp.Id">
|
||||||
Payouts
|
Payouts
|
||||||
</a> -
|
</a>
|
||||||
<a asp-action="ArchivePullPayment"
|
<a asp-action="ArchivePullPayment"
|
||||||
|
permission="@Policies.CanModifyStoreSettings"
|
||||||
asp-route-storeId="@Context.GetRouteValue("storeId")"
|
asp-route-storeId="@Context.GetRouteValue("storeId")"
|
||||||
asp-route-pullPaymentId="@pp.Id"
|
asp-route-pullPaymentId="@pp.Id"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
|
@using BTCPayServer.Client
|
||||||
<nav id="sideNav" class="nav flex-column mb-4">
|
<nav id="sideNav" class="nav flex-column mb-4">
|
||||||
<a id="Nav-@(nameof(StoreNavPages.PaymentMethods))" class="nav-link @ViewData.IsActivePage(StoreNavPages.PaymentMethods)" asp-controller="Stores" asp-action="PaymentMethods" asp-route-storeId="@Context.GetRouteValue("storeId")">Payment Methods</a>
|
<a permission="@Policies.CanModifyStoreSettings" id="Nav-@(nameof(StoreNavPages.PaymentMethods))" class="nav-link @ViewData.IsActivePage(StoreNavPages.PaymentMethods)" asp-controller="Stores" asp-action="PaymentMethods" asp-route-storeId="@Context.GetRouteValue("storeId")">Payment Methods</a>
|
||||||
<a id="Nav-@(nameof(StoreNavPages.Rates))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Rates)" asp-controller="Stores" asp-action="Rates" asp-route-storeId="@Context.GetRouteValue("storeId")">Rates</a>
|
<a permission="@Policies.CanModifyStoreSettings" id="Nav-@(nameof(StoreNavPages.Rates))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Rates)" asp-controller="Stores" asp-action="Rates" asp-route-storeId="@Context.GetRouteValue("storeId")">Rates</a>
|
||||||
<a id="Nav-@(nameof(StoreNavPages.CheckoutAppearance))" class="nav-link @ViewData.IsActivePage(StoreNavPages.CheckoutAppearance)" asp-controller="Stores" asp-action="CheckoutAppearance" asp-route-storeId="@Context.GetRouteValue("storeId")">Checkout Appearance</a>
|
<a permission="@Policies.CanModifyStoreSettings" id="Nav-@(nameof(StoreNavPages.CheckoutAppearance))" class="nav-link @ViewData.IsActivePage(StoreNavPages.CheckoutAppearance)" asp-controller="Stores" asp-action="CheckoutAppearance" asp-route-storeId="@Context.GetRouteValue("storeId")">Checkout Appearance</a>
|
||||||
<a id="Nav-@(nameof(StoreNavPages.GeneralSettings))" class="nav-link @ViewData.IsActivePage(StoreNavPages.GeneralSettings)" asp-controller="Stores" asp-action="GeneralSettings" asp-route-storeId="@Context.GetRouteValue("storeId")">General Settings</a>
|
<a permission="@Policies.CanModifyStoreSettings" id="Nav-@(nameof(StoreNavPages.GeneralSettings))" class="nav-link @ViewData.IsActivePage(StoreNavPages.GeneralSettings)" asp-controller="Stores" asp-action="GeneralSettings" asp-route-storeId="@Context.GetRouteValue("storeId")">General Settings</a>
|
||||||
<a id="Nav-@(nameof(StoreNavPages.Tokens))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Tokens)" asp-controller="Stores" asp-action="ListTokens" asp-route-storeId="@Context.GetRouteValue("storeId")">Access Tokens</a>
|
<a permission="@Policies.CanModifyStoreSettings" id="Nav-@(nameof(StoreNavPages.Tokens))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Tokens)" asp-controller="Stores" asp-action="ListTokens" asp-route-storeId="@Context.GetRouteValue("storeId")">Access Tokens</a>
|
||||||
<a id="Nav-@(nameof(StoreNavPages.Users))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Users)" asp-controller="Stores" asp-action="StoreUsers" asp-route-storeId="@Context.GetRouteValue("storeId")">Users</a>
|
<a permission="@Policies.CanModifyStoreSettings" id="Nav-@(nameof(StoreNavPages.Users))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Users)" asp-controller="Stores" asp-action="StoreUsers" asp-route-storeId="@Context.GetRouteValue("storeId")">Users</a>
|
||||||
<a id="Nav-@(nameof(StoreNavPages.PayButton))" class="nav-link @ViewData.IsActivePage(StoreNavPages.PayButton)" asp-controller="Stores" asp-action="PayButton" asp-route-storeId="@Context.GetRouteValue("storeId")">Pay Button</a>
|
<a permission="@Policies.CanModifyStoreSettings" id="Nav-@(nameof(StoreNavPages.PayButton))" class="nav-link @ViewData.IsActivePage(StoreNavPages.PayButton)" asp-controller="Stores" asp-action="PayButton" asp-route-storeId="@Context.GetRouteValue("storeId")">Pay Button</a>
|
||||||
<a id="Nav-@(nameof(StoreNavPages.Integrations))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Integrations)" asp-controller="Stores" asp-action="Integrations" asp-route-storeId="@Context.GetRouteValue("storeId")">Integrations</a>
|
<a permission="@Policies.CanModifyStoreSettings" id="Nav-@(nameof(StoreNavPages.Integrations))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Integrations)" asp-controller="Stores" asp-action="Integrations" asp-route-storeId="@Context.GetRouteValue("storeId")">Integrations</a>
|
||||||
<a id="Nav-@(nameof(StoreNavPages.Webhooks))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Webhooks)" asp-controller="Stores" asp-action="Webhooks" asp-route-storeId="@Context.GetRouteValue("storeId")">Webhooks</a>
|
<a permission="@Policies.CanModifyStoreSettings" id="Nav-@(nameof(StoreNavPages.Webhooks))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Webhooks)" asp-controller="Stores" asp-action="Webhooks" asp-route-storeId="@Context.GetRouteValue("storeId")">Webhooks</a>
|
||||||
<a id="Nav-@(nameof(StoreNavPages.PullPayments))" class="nav-link @ViewData.IsActivePage(StoreNavPages.PullPayments)" asp-action="PullPayments" asp-controller="StorePullPayments" asp-route-storeId="@Context.GetRouteValue("storeId")">Pull Payments</a>
|
<a id="Nav-@(nameof(StoreNavPages.PullPayments))" class="nav-link @ViewData.IsActivePage(StoreNavPages.PullPayments)" asp-action="PullPayments" asp-controller="StorePullPayments" asp-route-storeId="@Context.GetRouteValue("storeId")">Pull Payments</a>
|
||||||
<a id="Nav-@(nameof(StoreNavPages.Payouts))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Payouts)" asp-action="Payouts" asp-controller="StorePullPayments" asp-route-storeId="@Context.GetRouteValue("storeId")">Payouts</a>
|
<a id="Nav-@(nameof(StoreNavPages.Payouts))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Payouts)" asp-action="Payouts" asp-controller="StorePullPayments" asp-route-storeId="@Context.GetRouteValue("storeId")">Payouts</a>
|
||||||
<vc:ui-extension-point location="store-nav" model="@Model" />
|
<vc:ui-extension-point location="store-nav" model="@Model"/>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
@ -75,9 +75,10 @@
|
||||||
@if (store.IsOwner)
|
@if (store.IsOwner)
|
||||||
{
|
{
|
||||||
<a asp-action="PaymentMethods" asp-controller="Stores" asp-route-storeId="@store.Id" id="update-store-@store.Id">Settings</a><span> - </span>
|
<a asp-action="PaymentMethods" asp-controller="Stores" asp-route-storeId="@store.Id" id="update-store-@store.Id">Settings</a><span> - </span>
|
||||||
|
<a asp-action="DeleteStore" asp-controller="Stores" asp-route-storeId="@store.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The store <strong>@store.Name</strong> will be permanently deleted. This action will also delete all invoices, apps and data associated with the store." data-confirm-input="DELETE">Delete</a><span> - </span>
|
||||||
}
|
}
|
||||||
<a asp-action="DeleteStore" asp-controller="Stores" asp-route-storeId="@store.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The store <strong>@store.Name</strong> will be permanently deleted. This action will also delete all invoices, apps and data associated with the store." data-confirm-input="DELETE">Delete</a>
|
<a asp-action="PullPayments" asp-controller="StorePullPayments" asp-route-storeId="@store.Id" >Pull Payments</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
Loading…
Add table
Reference in a new issue