diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index befa6f23f..f37eede0a 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -501,19 +501,19 @@ namespace BTCPayServer.Tests // Check users list 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); // Check user which didn't require approval s.Driver.FindElement(By.Id("SearchTerm")).Clear(); s.Driver.FindElement(By.Id("SearchTerm")).SendKeys(autoApproved.RegisterDetails.Email); 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.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 - 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")); // 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")).SendKeys(unapproved.RegisterDetails.Email); 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.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 - 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("SaveUser")).Click(); Assert.Contains("User successfully updated", s.FindAlertMessage().Text); // Check list again s.GoToServer(ServerNavPages.Users); 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.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 s.Logout(); diff --git a/BTCPayServer/Components/StoreSelector/Default.cshtml b/BTCPayServer/Components/StoreSelector/Default.cshtml index 50e3ae140..ceeb44d99 100644 --- a/BTCPayServer/Components/StoreSelector/Default.cshtml +++ b/BTCPayServer/Components/StoreSelector/Default.cshtml @@ -2,6 +2,7 @@ @using BTCPayServer.Client @using BTCPayServer.Components.MainLogo @using BTCPayServer.Services +@using BTCPayServer.Views.Server @using BTCPayServer.Views.Stores @inject BTCPayServerEnvironment Env @inject IFileService FileService @@ -53,7 +54,7 @@ else @foreach (var option in Model.Options) {
  • - @StoreName(option.Text) + @StoreName(option.Text)
  • } @if (Model.Options.Any()) @@ -66,6 +67,10 @@ else
  • @Model.ArchivedCount Archived Store@(Model.ArchivedCount == 1 ? "" : "s")
  • } + @* +
  • +
  • Admin Store Overview
  • + *@ diff --git a/BTCPayServer/Controllers/UIServerController.Users.cs b/BTCPayServer/Controllers/UIServerController.Users.cs index a6ff2246c..2869da0d9 100644 --- a/BTCPayServer/Controllers/UIServerController.Users.cs +++ b/BTCPayServer/Controllers/UIServerController.Users.cs @@ -52,6 +52,8 @@ namespace BTCPayServer.Controllers model.Roles = roleManager.Roles.ToDictionary(role => role.Id, role => role.Name); model.Users = await usersQuery .Include(user => user.UserRoles) + .Include(user => user.UserStores) + .ThenInclude(data => data.StoreData) .Skip(model.Skip) .Take(model.Count) .Select(u => new UsersViewModel.UserViewModel @@ -63,7 +65,8 @@ namespace BTCPayServer.Controllers Approved = u.RequiresApproval ? u.Approved : null, Created = u.Created, 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(); diff --git a/BTCPayServer/Controllers/UIServerController.cs b/BTCPayServer/Controllers/UIServerController.cs index d1ae86dbb..5d9bb6d61 100644 --- a/BTCPayServer/Controllers/UIServerController.cs +++ b/BTCPayServer/Controllers/UIServerController.cs @@ -20,6 +20,7 @@ using BTCPayServer.Hosting; using BTCPayServer.Logging; using BTCPayServer.Models; using BTCPayServer.Models.ServerViewModels; +using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Payments; using BTCPayServer.Services; using BTCPayServer.Services.Apps; @@ -122,6 +123,26 @@ namespace BTCPayServer.Controllers _transactionLinkProviders = transactionLinkProviders; } + [HttpGet("server/stores")] + public async Task 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")] public IActionResult Maintenance() { diff --git a/BTCPayServer/Controllers/UIStoresController.cs b/BTCPayServer/Controllers/UIStoresController.cs index c499d1fd1..fba2572bc 100644 --- a/BTCPayServer/Controllers/UIStoresController.cs +++ b/BTCPayServer/Controllers/UIStoresController.cs @@ -150,6 +150,7 @@ namespace BTCPayServer.Controllers { return Forbid(); } + HttpContext.SetStoreData(store); if (store.GetPermissionSet(userId).Contains(Policies.CanModifyStoreSettings, storeId)) { return RedirectToAction("Dashboard", new { storeId }); @@ -158,7 +159,6 @@ namespace BTCPayServer.Controllers { return RedirectToAction("ListInvoices", "UIInvoice", new { storeId }); } - HttpContext.SetStoreData(store); return View(); } diff --git a/BTCPayServer/Controllers/UIUserStoresController.cs b/BTCPayServer/Controllers/UIUserStoresController.cs index b795803e7..decf66cec 100644 --- a/BTCPayServer/Controllers/UIUserStoresController.cs +++ b/BTCPayServer/Controllers/UIUserStoresController.cs @@ -35,7 +35,7 @@ namespace BTCPayServer.Controllers _rateFactory = rateFactory; } - [HttpGet()] + [HttpGet] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettingsUnscoped)] public async Task ListStores(bool archived = false) { diff --git a/BTCPayServer/Models/ServerViewModels/UsersViewModel.cs b/BTCPayServer/Models/ServerViewModels/UsersViewModel.cs index d4d600028..6736b5e0f 100644 --- a/BTCPayServer/Models/ServerViewModels/UsersViewModel.cs +++ b/BTCPayServer/Models/ServerViewModels/UsersViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using BTCPayServer.Data; using BTCPayServer.Services.Stores; namespace BTCPayServer.Models.ServerViewModels @@ -17,6 +18,7 @@ namespace BTCPayServer.Models.ServerViewModels public bool IsAdmin { get; set; } public DateTimeOffset? Created { get; set; } public IEnumerable Roles { get; set; } + public IEnumerable Stores { get; set; } } public List Users { get; set; } = new List(); public override int CurrentPageCount => Users.Count; diff --git a/BTCPayServer/Models/StoreViewModels/ListStoresViewModel.cs b/BTCPayServer/Models/StoreViewModels/ListStoresViewModel.cs index c822591c2..a7f811182 100644 --- a/BTCPayServer/Models/StoreViewModels/ListStoresViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/ListStoresViewModel.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using BTCPayServer.Data; namespace BTCPayServer.Models.StoreViewModels; @@ -9,6 +10,7 @@ public class ListStoresViewModel public string StoreName { get; set; } public string StoreId { get; set; } public bool Archived { get; set; } + public List Users { get; set; } } public List Stores { get; set; } = new (); diff --git a/BTCPayServer/Security/CookieAuthorizationHandler.cs b/BTCPayServer/Security/CookieAuthorizationHandler.cs index 9391f4866..2a09bf6f1 100644 --- a/BTCPayServer/Security/CookieAuthorizationHandler.cs +++ b/BTCPayServer/Security/CookieAuthorizationHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Contracts; @@ -125,7 +126,10 @@ namespace BTCPayServer.Security 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) @@ -164,14 +168,15 @@ namespace BTCPayServer.Security { if (store != null) { - _httpContext.SetStoreData(store); + if (_httpContext.GetStoreData()?.Id != store.Id) + _httpContext.SetStoreData(store); // cache associated entities if present - if (app != null) + if (app != null && _httpContext.GetAppData()?.Id != app.Id) _httpContext.SetAppData(app); - if (invoice != null) + if (invoice != null && _httpContext.GetInvoiceData()?.Id != invoice.Id) _httpContext.SetInvoiceData(invoice); - if (paymentRequest != null) + if (paymentRequest != null && _httpContext.GetPaymentRequestData()?.Id != paymentRequest.Id) _httpContext.SetPaymentRequestData(paymentRequest); } } diff --git a/BTCPayServer/Services/Stores/StoreRepository.cs b/BTCPayServer/Services/Stores/StoreRepository.cs index 533fb1d2b..571dc8004 100644 --- a/BTCPayServer/Services/Stores/StoreRepository.cs +++ b/BTCPayServer/Services/Stores/StoreRepository.cs @@ -201,6 +201,18 @@ namespace BTCPayServer.Services.Stores }; } + public async Task GetStores(IEnumerable? 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 GetStoresByUserId(string userId, IEnumerable? storeIds = null) { await using var ctx = _ContextFactory.CreateContext(); diff --git a/BTCPayServer/Views/UIServer/ListStores.cshtml b/BTCPayServer/Views/UIServer/ListStores.cshtml new file mode 100644 index 000000000..29f7e8da6 --- /dev/null +++ b/BTCPayServer/Views/UIServer/ListStores.cshtml @@ -0,0 +1,79 @@ +@model BTCPayServer.Models.StoreViewModels.ListStoresViewModel +@{ + Layout = "_Layout"; + ViewData.SetActivePage(ServerNavPages.Stores, "Store Overview"); +} + + + +
    +

    + @ViewData["Title"] +

    +
    +@if (Model.Stores.Any()) +{ + + + + + + + + + @foreach (var store in Model.Stores) + { + var detailsId = $"store_details_{store.StoreId}"; + + + + + + + + + } + +
    StoreUsers
    + + @store.StoreName + + @if (store.Archived) + { + archived + } + @store.Users.Count User@(store.Users.Count == 1 ? "" : "s") +
    + +
    +
    + @if (store.Users.Any()) + { +
      + @foreach (var user in store.Users) + { +
    • + @user.ApplicationUser.Email + @user.StoreRoleId + @if (store.Archived) + { + archived + } +
    • + } +
    + } + else + { + No users + } +
    +} +else +{ +

    + There are no stores yet. +

    +} diff --git a/BTCPayServer/Views/UIServer/ListUsers.cshtml b/BTCPayServer/Views/UIServer/ListUsers.cshtml index dc696d517..34a444044 100644 --- a/BTCPayServer/Views/UIServer/ListUsers.cshtml +++ b/BTCPayServer/Views/UIServer/ListUsers.cshtml @@ -19,7 +19,6 @@ const string sortByDesc = "Sort by descending..."; const string sortByAsc = "Sort by ascending..."; } -

    @ViewData["Title"]

    @@ -46,9 +45,11 @@ + Stores Created Status + @@ -61,14 +62,18 @@ { EmailConfirmed: false } => ("Pending Email Verification", "warning"), _ => ("Active", "success") }; - - - @user.Email - @foreach (var role in user.Roles) - { - @Model.Roles[role] - } + var detailsId = $"user_details_{user.Id}"; + + +
    + @user.Email + @foreach (var role in user.Roles) + { + @Model.Roles[role] + } +
    + @user.Stores.Count() Store@(user.Stores.Count() == 1 ? "" : "s") @user.Created?.ToBrowserDate() @status.Item1 @@ -90,6 +95,37 @@ Remove
    + +
    + +
    + + + + + @if (user.Stores.Any()) + { +
      + @foreach (var store in user.Stores) + { +
    • + @store.StoreData.StoreName + @store.StoreRoleId + @if (store.StoreData.Archived) + { + archived + } +
    • + } +
    + } + else + { + No stores + } + } diff --git a/BTCPayServer/Views/UIServer/ServerNavPages.cs b/BTCPayServer/Views/UIServer/ServerNavPages.cs index 074003e6a..73ea58ee2 100644 --- a/BTCPayServer/Views/UIServer/ServerNavPages.cs +++ b/BTCPayServer/Views/UIServer/ServerNavPages.cs @@ -3,6 +3,7 @@ namespace BTCPayServer.Views.Server public enum ServerNavPages { Index, Users, Emails, Policies, Branding, Services, Maintenance, Logs, Files, Plugins, - Roles + Roles, + Stores } }