mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 06:24:24 +01:00
Admin overview of the stores on the instance (#5745)
* Admin overview of the stores on the instance POC/Draft for #5674. * Enable admin to access foreign stores * Remove stores list link * UI updates * Grant admins guest access to foreign stores * Optimize cookie auth handler * Test fix * Revert changes related to StoreRepository.FindStore with isAdmin
This commit is contained in:
@@ -501,19 +501,19 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
// Check users list
|
// Check users list
|
||||||
s.GoToServer(ServerNavPages.Users);
|
s.GoToServer(ServerNavPages.Users);
|
||||||
var rows = s.Driver.FindElements(By.CssSelector("#UsersList tr"));
|
var rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
|
||||||
Assert.True(rows.Count >= 3);
|
Assert.True(rows.Count >= 3);
|
||||||
|
|
||||||
// Check user which didn't require approval
|
// Check user which didn't require approval
|
||||||
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
|
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
|
||||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(autoApproved.RegisterDetails.Email);
|
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(autoApproved.RegisterDetails.Email);
|
||||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
|
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
|
||||||
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr"));
|
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
|
||||||
Assert.Single(rows);
|
Assert.Single(rows);
|
||||||
Assert.Contains(autoApproved.RegisterDetails.Email, rows.First().Text);
|
Assert.Contains(autoApproved.RegisterDetails.Email, rows.First().Text);
|
||||||
s.Driver.ElementDoesNotExist(By.CssSelector("#UsersList tr:first-child .user-approved"));
|
s.Driver.ElementDoesNotExist(By.CssSelector("#UsersList tr.user-overview-row:first-child .user-approved"));
|
||||||
// Edit view does not contain approve toggle
|
// Edit view does not contain approve toggle
|
||||||
s.Driver.FindElement(By.CssSelector("#UsersList tr:first-child .user-edit")).Click();
|
s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .user-edit")).Click();
|
||||||
s.Driver.ElementDoesNotExist(By.Id("Approved"));
|
s.Driver.ElementDoesNotExist(By.Id("Approved"));
|
||||||
|
|
||||||
// Check user which still requires approval
|
// Check user which still requires approval
|
||||||
@@ -521,22 +521,22 @@ namespace BTCPayServer.Tests
|
|||||||
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
|
s.Driver.FindElement(By.Id("SearchTerm")).Clear();
|
||||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(unapproved.RegisterDetails.Email);
|
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(unapproved.RegisterDetails.Email);
|
||||||
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
|
s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(Keys.Enter);
|
||||||
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr"));
|
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
|
||||||
Assert.Single(rows);
|
Assert.Single(rows);
|
||||||
Assert.Contains(unapproved.RegisterDetails.Email, rows.First().Text);
|
Assert.Contains(unapproved.RegisterDetails.Email, rows.First().Text);
|
||||||
Assert.Contains("Pending Approval", s.Driver.FindElement(By.CssSelector("#UsersList tr:first-child .user-status")).Text);
|
Assert.Contains("Pending Approval", s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .user-status")).Text);
|
||||||
// Approve user
|
// Approve user
|
||||||
s.Driver.FindElement(By.CssSelector("#UsersList tr:first-child .user-edit")).Click();
|
s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .user-edit")).Click();
|
||||||
s.Driver.FindElement(By.Id("Approved")).Click();
|
s.Driver.FindElement(By.Id("Approved")).Click();
|
||||||
s.Driver.FindElement(By.Id("SaveUser")).Click();
|
s.Driver.FindElement(By.Id("SaveUser")).Click();
|
||||||
Assert.Contains("User successfully updated", s.FindAlertMessage().Text);
|
Assert.Contains("User successfully updated", s.FindAlertMessage().Text);
|
||||||
// Check list again
|
// Check list again
|
||||||
s.GoToServer(ServerNavPages.Users);
|
s.GoToServer(ServerNavPages.Users);
|
||||||
Assert.Contains(unapproved.RegisterDetails.Email, s.Driver.FindElement(By.Id("SearchTerm")).GetAttribute("value"));
|
Assert.Contains(unapproved.RegisterDetails.Email, s.Driver.FindElement(By.Id("SearchTerm")).GetAttribute("value"));
|
||||||
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr"));
|
rows = s.Driver.FindElements(By.CssSelector("#UsersList tr.user-overview-row"));
|
||||||
Assert.Single(rows);
|
Assert.Single(rows);
|
||||||
Assert.Contains(unapproved.RegisterDetails.Email, rows.First().Text);
|
Assert.Contains(unapproved.RegisterDetails.Email, rows.First().Text);
|
||||||
Assert.Contains("Active", s.Driver.FindElement(By.CssSelector("#UsersList tr:first-child .user-status")).Text);
|
Assert.Contains("Active", s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .user-status")).Text);
|
||||||
|
|
||||||
// Finally, login user that needed approval
|
// Finally, login user that needed approval
|
||||||
s.Logout();
|
s.Logout();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
@using BTCPayServer.Client
|
@using BTCPayServer.Client
|
||||||
@using BTCPayServer.Components.MainLogo
|
@using BTCPayServer.Components.MainLogo
|
||||||
@using BTCPayServer.Services
|
@using BTCPayServer.Services
|
||||||
|
@using BTCPayServer.Views.Server
|
||||||
@using BTCPayServer.Views.Stores
|
@using BTCPayServer.Views.Stores
|
||||||
@inject BTCPayServerEnvironment Env
|
@inject BTCPayServerEnvironment Env
|
||||||
@inject IFileService FileService
|
@inject IFileService FileService
|
||||||
@@ -53,7 +54,7 @@ else
|
|||||||
@foreach (var option in Model.Options)
|
@foreach (var option in Model.Options)
|
||||||
{
|
{
|
||||||
<li>
|
<li>
|
||||||
<a asp-controller="UIStores" asp-action="Index" asp-route-storeId="@option.Value" class="dropdown-item@(option.Selected ? " active" : "")" id="StoreSelectorMenuItem-@option.Value">@StoreName(option.Text)</a>
|
<a asp-controller="UIStores" asp-action="Index" asp-route-storeId="@option.Value" class="dropdown-item@(option.Selected && ViewData.IsActivePage(ServerNavPages.Stores) != "active" ? " active" : "")" id="StoreSelectorMenuItem-@option.Value">@StoreName(option.Text)</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
@if (Model.Options.Any())
|
@if (Model.Options.Any())
|
||||||
@@ -66,6 +67,10 @@ else
|
|||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li><a asp-controller="UIUserStores" asp-action="ListStores" asp-route-archived="true" class="dropdown-item @ViewData.IsActivePage(StoreNavPages.Index)" id="StoreSelectorArchived">@Model.ArchivedCount Archived Store@(Model.ArchivedCount == 1 ? "" : "s")</a></li>
|
<li><a asp-controller="UIUserStores" asp-action="ListStores" asp-route-archived="true" class="dropdown-item @ViewData.IsActivePage(StoreNavPages.Index)" id="StoreSelectorArchived">@Model.ArchivedCount Archived Store@(Model.ArchivedCount == 1 ? "" : "s")</a></li>
|
||||||
}
|
}
|
||||||
|
@*
|
||||||
|
<li permission="@Policies.CanModifyServerSettings"><hr class="dropdown-divider"></li>
|
||||||
|
<li permission="@Policies.CanModifyServerSettings"><a asp-controller="UIServer" asp-action="ListStores" class="dropdown-item @ViewData.IsActivePage(ServerNavPages.Stores)" id="StoreSelectorAdminStores">Admin Store Overview</a></li>
|
||||||
|
*@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ namespace BTCPayServer.Controllers
|
|||||||
model.Roles = roleManager.Roles.ToDictionary(role => role.Id, role => role.Name);
|
model.Roles = roleManager.Roles.ToDictionary(role => role.Id, role => role.Name);
|
||||||
model.Users = await usersQuery
|
model.Users = await usersQuery
|
||||||
.Include(user => user.UserRoles)
|
.Include(user => user.UserRoles)
|
||||||
|
.Include(user => user.UserStores)
|
||||||
|
.ThenInclude(data => data.StoreData)
|
||||||
.Skip(model.Skip)
|
.Skip(model.Skip)
|
||||||
.Take(model.Count)
|
.Take(model.Count)
|
||||||
.Select(u => new UsersViewModel.UserViewModel
|
.Select(u => new UsersViewModel.UserViewModel
|
||||||
@@ -63,7 +65,8 @@ namespace BTCPayServer.Controllers
|
|||||||
Approved = u.RequiresApproval ? u.Approved : null,
|
Approved = u.RequiresApproval ? u.Approved : null,
|
||||||
Created = u.Created,
|
Created = u.Created,
|
||||||
Roles = u.UserRoles.Select(role => role.RoleId),
|
Roles = u.UserRoles.Select(role => role.RoleId),
|
||||||
Disabled = u.LockoutEnabled && u.LockoutEnd != null && DateTimeOffset.UtcNow < u.LockoutEnd.Value.UtcDateTime
|
Disabled = u.LockoutEnabled && u.LockoutEnd != null && DateTimeOffset.UtcNow < u.LockoutEnd.Value.UtcDateTime,
|
||||||
|
Stores = u.UserStores.OrderBy(s => !s.StoreData.Archived).ToList()
|
||||||
})
|
})
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ using BTCPayServer.Hosting;
|
|||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
using BTCPayServer.Models;
|
using BTCPayServer.Models;
|
||||||
using BTCPayServer.Models.ServerViewModels;
|
using BTCPayServer.Models.ServerViewModels;
|
||||||
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Apps;
|
using BTCPayServer.Services.Apps;
|
||||||
@@ -122,6 +123,26 @@ namespace BTCPayServer.Controllers
|
|||||||
_transactionLinkProviders = transactionLinkProviders;
|
_transactionLinkProviders = transactionLinkProviders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("server/stores")]
|
||||||
|
public async Task<IActionResult> ListStores()
|
||||||
|
{
|
||||||
|
var stores = await _StoreRepository.GetStores();
|
||||||
|
var vm = new ListStoresViewModel
|
||||||
|
{
|
||||||
|
Stores = stores
|
||||||
|
.Select(s => new ListStoresViewModel.StoreViewModel
|
||||||
|
{
|
||||||
|
StoreId = s.Id,
|
||||||
|
StoreName = s.StoreName,
|
||||||
|
Archived = s.Archived,
|
||||||
|
Users = s.UserStores
|
||||||
|
})
|
||||||
|
.OrderBy(s => !s.Archived)
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("server/maintenance")]
|
[HttpGet("server/maintenance")]
|
||||||
public IActionResult Maintenance()
|
public IActionResult Maintenance()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
return Forbid();
|
return Forbid();
|
||||||
}
|
}
|
||||||
|
HttpContext.SetStoreData(store);
|
||||||
if (store.GetPermissionSet(userId).Contains(Policies.CanModifyStoreSettings, storeId))
|
if (store.GetPermissionSet(userId).Contains(Policies.CanModifyStoreSettings, storeId))
|
||||||
{
|
{
|
||||||
return RedirectToAction("Dashboard", new { storeId });
|
return RedirectToAction("Dashboard", new { storeId });
|
||||||
@@ -158,7 +159,6 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
return RedirectToAction("ListInvoices", "UIInvoice", new { storeId });
|
return RedirectToAction("ListInvoices", "UIInvoice", new { storeId });
|
||||||
}
|
}
|
||||||
HttpContext.SetStoreData(store);
|
|
||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ namespace BTCPayServer.Controllers
|
|||||||
_rateFactory = rateFactory;
|
_rateFactory = rateFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet()]
|
[HttpGet]
|
||||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettingsUnscoped)]
|
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettingsUnscoped)]
|
||||||
public async Task<IActionResult> ListStores(bool archived = false)
|
public async Task<IActionResult> ListStores(bool archived = false)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.ServerViewModels
|
namespace BTCPayServer.Models.ServerViewModels
|
||||||
@@ -17,6 +18,7 @@ namespace BTCPayServer.Models.ServerViewModels
|
|||||||
public bool IsAdmin { get; set; }
|
public bool IsAdmin { get; set; }
|
||||||
public DateTimeOffset? Created { get; set; }
|
public DateTimeOffset? Created { get; set; }
|
||||||
public IEnumerable<string> Roles { get; set; }
|
public IEnumerable<string> Roles { get; set; }
|
||||||
|
public IEnumerable<UserStore> Stores { get; set; }
|
||||||
}
|
}
|
||||||
public List<UserViewModel> Users { get; set; } = new List<UserViewModel>();
|
public List<UserViewModel> Users { get; set; } = new List<UserViewModel>();
|
||||||
public override int CurrentPageCount => Users.Count;
|
public override int CurrentPageCount => Users.Count;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.StoreViewModels;
|
namespace BTCPayServer.Models.StoreViewModels;
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ public class ListStoresViewModel
|
|||||||
public string StoreName { get; set; }
|
public string StoreName { get; set; }
|
||||||
public string StoreId { get; set; }
|
public string StoreId { get; set; }
|
||||||
public bool Archived { get; set; }
|
public bool Archived { get; set; }
|
||||||
|
public List<UserStore> Users { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<StoreViewModel> Stores { get; set; } = new ();
|
public List<StoreViewModel> Stores { get; set; } = new ();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
using BTCPayServer.Abstractions.Contracts;
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
@@ -125,7 +126,10 @@ namespace BTCPayServer.Security
|
|||||||
|
|
||||||
if (!string.IsNullOrEmpty(storeId))
|
if (!string.IsNullOrEmpty(storeId))
|
||||||
{
|
{
|
||||||
store = await _storeRepository.FindStore(storeId, userId);
|
var cachedStore = _httpContext.GetStoreData();
|
||||||
|
store = cachedStore?.Id == storeId
|
||||||
|
? cachedStore
|
||||||
|
: await _storeRepository.FindStore(storeId, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Policies.IsServerPolicy(policy) && isAdmin)
|
if (Policies.IsServerPolicy(policy) && isAdmin)
|
||||||
@@ -164,14 +168,15 @@ namespace BTCPayServer.Security
|
|||||||
{
|
{
|
||||||
if (store != null)
|
if (store != null)
|
||||||
{
|
{
|
||||||
|
if (_httpContext.GetStoreData()?.Id != store.Id)
|
||||||
_httpContext.SetStoreData(store);
|
_httpContext.SetStoreData(store);
|
||||||
|
|
||||||
// cache associated entities if present
|
// cache associated entities if present
|
||||||
if (app != null)
|
if (app != null && _httpContext.GetAppData()?.Id != app.Id)
|
||||||
_httpContext.SetAppData(app);
|
_httpContext.SetAppData(app);
|
||||||
if (invoice != null)
|
if (invoice != null && _httpContext.GetInvoiceData()?.Id != invoice.Id)
|
||||||
_httpContext.SetInvoiceData(invoice);
|
_httpContext.SetInvoiceData(invoice);
|
||||||
if (paymentRequest != null)
|
if (paymentRequest != null && _httpContext.GetPaymentRequestData()?.Id != paymentRequest.Id)
|
||||||
_httpContext.SetPaymentRequestData(paymentRequest);
|
_httpContext.SetPaymentRequestData(paymentRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,6 +201,18 @@ namespace BTCPayServer.Services.Stores
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<StoreData[]> GetStores(IEnumerable<string>? storeIds = null)
|
||||||
|
{
|
||||||
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
|
return await ctx.Stores
|
||||||
|
.Where(s => storeIds == null || storeIds.Contains(s.Id))
|
||||||
|
.Include(data => data.UserStores)
|
||||||
|
.ThenInclude(data => data.StoreRole)
|
||||||
|
.Include(data => data.UserStores)
|
||||||
|
.ThenInclude(data => data.ApplicationUser)
|
||||||
|
.ToArrayAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<StoreData[]> GetStoresByUserId(string userId, IEnumerable<string>? storeIds = null)
|
public async Task<StoreData[]> GetStoresByUserId(string userId, IEnumerable<string>? storeIds = null)
|
||||||
{
|
{
|
||||||
await using var ctx = _ContextFactory.CreateContext();
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
|
|||||||
79
BTCPayServer/Views/UIServer/ListStores.cshtml
Normal file
79
BTCPayServer/Views/UIServer/ListStores.cshtml
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
@model BTCPayServer.Models.StoreViewModels.ListStoresViewModel
|
||||||
|
@{
|
||||||
|
Layout = "_Layout";
|
||||||
|
ViewData.SetActivePage(ServerNavPages.Stores, "Store Overview");
|
||||||
|
}
|
||||||
|
|
||||||
|
<partial name="_StatusMessage" />
|
||||||
|
|
||||||
|
<div class="d-sm-flex justify-content-between mb-2">
|
||||||
|
<h2 class="mb-0">
|
||||||
|
@ViewData["Title"]
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
@if (Model.Stores.Any())
|
||||||
|
{
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Store</th>
|
||||||
|
<th>Users</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var store in Model.Stores)
|
||||||
|
{
|
||||||
|
var detailsId = $"store_details_{store.StoreId}";
|
||||||
|
<tr id="store_@store.StoreId" class="mass-action-row">
|
||||||
|
<td>
|
||||||
|
<a asp-controller="UIStores" asp-action="Index" asp-route-storeId="@store.StoreId" id="Store-@store.StoreId">
|
||||||
|
@store.StoreName
|
||||||
|
</a>
|
||||||
|
@if (store.Archived)
|
||||||
|
{
|
||||||
|
<span class="badge bg-info ms-2">archived</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>@store.Users.Count User@(store.Users.Count == 1 ? "" : "s")</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="d-inline-flex align-items-center gap-2">
|
||||||
|
<button class="accordion-button collapsed only-for-js ms-0 d-inline-block" type="button" data-bs-toggle="collapse" data-bs-target="#@detailsId" aria-expanded="false" aria-controls="@detailsId">
|
||||||
|
<vc:icon symbol="caret-down" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr id="@detailsId" class="store-details-row collapse">
|
||||||
|
<td colspan="5" class="border-top-0">
|
||||||
|
@if (store.Users.Any())
|
||||||
|
{
|
||||||
|
<ul class="mb-0 p-0">
|
||||||
|
@foreach (var user in store.Users)
|
||||||
|
{
|
||||||
|
<li class="py-1 d-flex gap-2">
|
||||||
|
<a asp-controller="UIServer" asp-action="User" asp-route-userId="@user.ApplicationUser.Id">@user.ApplicationUser.Email</a>
|
||||||
|
<span class="badge bg-light">@user.StoreRoleId</span>
|
||||||
|
@if (store.Archived)
|
||||||
|
{
|
||||||
|
<span class="badge bg-info">archived</span>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="text-secondary">No users</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<p class="text-secondary mt-3">
|
||||||
|
There are no stores yet.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
@@ -19,7 +19,6 @@
|
|||||||
const string sortByDesc = "Sort by descending...";
|
const string sortByDesc = "Sort by descending...";
|
||||||
const string sortByAsc = "Sort by ascending...";
|
const string sortByAsc = "Sort by ascending...";
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||||
<h3 class="mb-0">@ViewData["Title"]</h3>
|
<h3 class="mb-0">@ViewData["Title"]</h3>
|
||||||
<a asp-action="CreateUser" class="btn btn-primary" role="button" id="CreateUser">
|
<a asp-action="CreateUser" class="btn btn-primary" role="button" id="CreateUser">
|
||||||
@@ -46,9 +45,11 @@
|
|||||||
<span class="fa @(sortIconClass)"></span>
|
<span class="fa @(sortIconClass)"></span>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
|
<th>Stores</th>
|
||||||
<th>Created</th>
|
<th>Created</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th class="actions-col"></th>
|
<th class="actions-col"></th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="UsersList">
|
<tbody id="UsersList">
|
||||||
@@ -61,14 +62,18 @@
|
|||||||
{ EmailConfirmed: false } => ("Pending Email Verification", "warning"),
|
{ EmailConfirmed: false } => ("Pending Email Verification", "warning"),
|
||||||
_ => ("Active", "success")
|
_ => ("Active", "success")
|
||||||
};
|
};
|
||||||
<tr>
|
var detailsId = $"user_details_{user.Id}";
|
||||||
<td class="d-flex align-items-center gap-2">
|
<tr id="user_@user.Id" class="user-overview-row mass-action-row">
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
<span class="user-email">@user.Email</span>
|
<span class="user-email">@user.Email</span>
|
||||||
@foreach (var role in user.Roles)
|
@foreach (var role in user.Roles)
|
||||||
{
|
{
|
||||||
<span class="badge bg-info">@Model.Roles[role]</span>
|
<span class="badge bg-info">@Model.Roles[role]</span>
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td>@user.Stores.Count() Store@(user.Stores.Count() == 1 ? "" : "s")</td>
|
||||||
<td>@user.Created?.ToBrowserDate()</td>
|
<td>@user.Created?.ToBrowserDate()</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="user-status badge bg-@status.Item2">@status.Item1</span>
|
<span class="user-status badge bg-@status.Item2">@status.Item1</span>
|
||||||
@@ -90,6 +95,37 @@
|
|||||||
<a asp-action="DeleteUser" asp-route-userId="@user.Id">Remove</a>
|
<a asp-action="DeleteUser" asp-route-userId="@user.Id">Remove</a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="d-inline-flex align-items-center gap-2">
|
||||||
|
<button class="accordion-button collapsed only-for-js ms-0 d-inline-block" type="button" data-bs-toggle="collapse" data-bs-target="#@detailsId" aria-expanded="false" aria-controls="@detailsId">
|
||||||
|
<vc:icon symbol="caret-down" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr id="@detailsId" class="user-details-row collapse">
|
||||||
|
<td colspan="6" class="border-top-0">
|
||||||
|
@if (user.Stores.Any())
|
||||||
|
{
|
||||||
|
<ul class="mb-0 p-0">
|
||||||
|
@foreach (var store in user.Stores)
|
||||||
|
{
|
||||||
|
<li class="py-1 d-flex gap-2">
|
||||||
|
<a asp-controller="UIStores" asp-action="Index" asp-route-storeId="@store.StoreData.Id">@store.StoreData.StoreName</a>
|
||||||
|
<span class="badge bg-light">@store.StoreRoleId</span>
|
||||||
|
@if (store.StoreData.Archived)
|
||||||
|
{
|
||||||
|
<span class="badge bg-info">archived</span>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="text-secondary">No stores</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ namespace BTCPayServer.Views.Server
|
|||||||
public enum ServerNavPages
|
public enum ServerNavPages
|
||||||
{
|
{
|
||||||
Index, Users, Emails, Policies, Branding, Services, Maintenance, Logs, Files, Plugins,
|
Index, Users, Emails, Policies, Branding, Services, Maintenance, Logs, Files, Plugins,
|
||||||
Roles
|
Roles,
|
||||||
|
Stores
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user