diff --git a/src/Admin/AdminConsole/AdminConsoleRootApp.razor b/src/Admin/AdminConsole/AdminConsoleRootApp.razor
deleted file mode 100644
index de1fce1f57..0000000000
--- a/src/Admin/AdminConsole/AdminConsoleRootApp.razor
+++ /dev/null
@@ -1,35 +0,0 @@
-@using Microsoft.AspNetCore.Components.Web
-@inject IHttpContextAccessor HttpContextAccessor
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Admin/AdminConsole/App.razor b/src/Admin/AdminConsole/App.razor
new file mode 100644
index 0000000000..7a895a78e4
--- /dev/null
+++ b/src/Admin/AdminConsole/App.razor
@@ -0,0 +1,92 @@
+@using Bit.Admin.Services
+@using Microsoft.AspNetCore.Identity
+@using Bit.Admin.Components.Navigation
+@inject IHttpContextAccessor HttpContextAccessor
+@inject SignInManager SignInManager
+@inject Core.Settings.GlobalSettings GlobalSettings
+@inject IAccessControlService AccessControlService
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Admin/AdminConsole/App.razor.cs b/src/Admin/AdminConsole/App.razor.cs
new file mode 100644
index 0000000000..d139d5bc7e
--- /dev/null
+++ b/src/Admin/AdminConsole/App.razor.cs
@@ -0,0 +1,84 @@
+using Bit.Admin.Enums;
+using Microsoft.AspNetCore.Components;
+
+namespace Bit.Admin.AdminConsole;
+
+public partial class App : ComponentBase
+{
+ public HashSet NavItems { get; private set; }
+
+ protected override void OnInitialized()
+ {
+ var canViewUsers = AccessControlService.UserHasPermission(Permission.User_List_View);
+ var canViewOrgs = AccessControlService.UserHasPermission(Permission.Org_List_View);
+ var canViewProviders = AccessControlService.UserHasPermission(Permission.Provider_List_View);
+ var canChargeBraintree = AccessControlService.UserHasPermission(Permission.Tools_ChargeBrainTreeCustomer);
+ var canCreateTransaction = AccessControlService.UserHasPermission(Permission.Tools_CreateEditTransaction);
+ var canPromoteAdmin = AccessControlService.UserHasPermission(Permission.Tools_PromoteAdmin);
+ var canGenerateLicense = AccessControlService.UserHasPermission(Permission.Tools_GenerateLicenseFile);
+ var canManageTaxRates = AccessControlService.UserHasPermission(Permission.Tools_ManageTaxRates);
+ var canManageStripeSubscriptions =
+ AccessControlService.UserHasPermission(Permission.Tools_ManageStripeSubscriptions);
+ var canProcessStripeEvents = AccessControlService.UserHasPermission(Permission.Tools_ProcessStripeEvents);
+ var canMigrateProviders = AccessControlService.UserHasPermission(Permission.Tools_MigrateProviders);
+
+ var canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin ||
+ canGenerateLicense || canManageTaxRates || canManageStripeSubscriptions;
+
+ NavItems =
+ [
+ new() { Label = "Users", Link = "/users", Show = canViewUsers },
+
+ new() { Label = "Organizations", Link = "/organizations", Show = canViewOrgs },
+ new() { Label = "Providers", Link = "/providers", Show = canViewProviders && !GlobalSettings.SelfHosted },
+ new()
+ {
+ Label = "Tools",
+ Link = "/tools",
+ Show = canViewTools && !GlobalSettings.SelfHosted,
+ DropDownItems =
+ [
+ new()
+ {
+ Label = "Charge Braintree Customer",
+ Link = "/tools/chargebraintree",
+ Show = canChargeBraintree
+ },
+
+ new()
+ {
+ Label = "Create/Edit Transaction",
+ Link = "/tools/createtransaction",
+ Show = canCreateTransaction
+ },
+
+ new() { Label = "Promote Admin", Link = "/tools/promoteadmin", Show = canPromoteAdmin },
+ new()
+ {
+ Label = "Generate License File",
+ Link = "/tools/generatelicense",
+ Show = canGenerateLicense
+ },
+
+ new() { Label = "Manage Tax Rates", Link = "/tools/taxrate", Show = canManageTaxRates },
+
+ new()
+ {
+ Label = "Manage Stripe Subscriptions",
+ Link = "/tools/stripesubscriptions",
+ Show = canManageStripeSubscriptions
+ },
+
+ new()
+ {
+ Label = "Process Stripe Events",
+ Link = "/process-stripe-events",
+ Show = canProcessStripeEvents
+ },
+
+ new() { Label = "Migrate Providers", Link = "/tools/migrateproviders", Show = canMigrateProviders }
+ ]
+ }
+ ];
+ }
+}
diff --git a/src/Admin/AdminConsole/Components/Pages/Organizations/ListOrganizationsPage.razor b/src/Admin/AdminConsole/Components/Pages/Organizations/ListOrganizationsPage.razor
new file mode 100644
index 0000000000..5dd2e8da07
--- /dev/null
+++ b/src/Admin/AdminConsole/Components/Pages/Organizations/ListOrganizationsPage.razor
@@ -0,0 +1,158 @@
+@page "/organizations2"
+@using Bit.Core.Repositories
+@using Bit.Core.Settings
+@using Bit.Infrastructure.EntityFramework.AdminConsole.Models
+
+@inject IGlobalSettings GlobalSettings
+@inject IOrganizationRepository OrganizationRepository
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (!Model.SelfHosted)
+ {
+
+
+
+
+
+
+ }
+
+
+
+
+
+
+
+ Name |
+ Plan |
+ Seats |
+ Created |
+ Details |
+
+
+
+ @if (Model.Items is { Count: > 0 })
+ {
+ @foreach (var organization in Model.Items)
+ {
+
+
+ @organization.DisplayName()
+ |
+
+ @organization.Plan
+ |
+
+ @organization.Seats
+ |
+
+
+ @organization.CreationDate.ToShortDateString()
+
+ |
+
+ @if (!GlobalSettings.SelfHosted)
+ {
+ if (!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
+ {
+
+ }
+ else
+ {
+
+ }
+ }
+ @if (organization.MaxStorageGb is > 1)
+ {
+
+
+ }
+ else
+ {
+
+
+ }
+ @if (organization.Enabled)
+ {
+
+
+ }
+ else
+ {
+
+ }
+ @if (organization.TwoFactorIsEnabled())
+ {
+
+ }
+ else
+ {
+
+ }
+ |
+
+ }
+ }
+ else
+ {
+
+ No results to list. |
+
+ }
+
+
+
+
+
+
+
+
diff --git a/src/Admin/AdminConsole/Components/Pages/Organizations/ListOrganizationsPage.razor.cs b/src/Admin/AdminConsole/Components/Pages/Organizations/ListOrganizationsPage.razor.cs
new file mode 100644
index 0000000000..87b62e5c2a
--- /dev/null
+++ b/src/Admin/AdminConsole/Components/Pages/Organizations/ListOrganizationsPage.razor.cs
@@ -0,0 +1,92 @@
+using System.ComponentModel.DataAnnotations;
+using System.Net;
+using Bit.Admin.Models;
+using Bit.Core.AdminConsole.Entities;
+using Microsoft.AspNetCore.Components;
+
+namespace Bit.Admin.AdminConsole.Components.Pages.Organizations;
+
+public partial class ListOrganizationsPage : ComponentBase
+{
+ public const string SearchFormName = "search-form";
+
+ [SupplyParameterFromForm(FormName = SearchFormName)]
+ public SearchFormModel? SearchForm { get; set; }
+
+ public ViewModel Model { get; } = new();
+
+ protected override async Task OnInitializedAsync()
+ {
+ SearchForm ??= new SearchFormModel();
+ Model.SelfHosted = GlobalSettings.SelfHosted;
+ Model.Action = GlobalSettings.SelfHosted ? "View" : "Edit";
+ }
+
+ private async Task OnSearchAsync()
+ {
+ if (SearchForm!.Page < 1)
+ {
+ SearchForm.Page = 1;
+ }
+
+ if (SearchForm.Count < 1)
+ {
+ SearchForm.Count = 1;
+ }
+
+ var encodedName = WebUtility.HtmlEncode(SearchForm.Name);
+ var skip = (SearchForm.Page - 1) * SearchForm.Count;
+ Model.Items = (List)await OrganizationRepository.SearchAsync(
+ encodedName,
+ SearchForm.Email,
+ SearchForm.Paid, skip, SearchForm.Count);
+
+ Model.Page = SearchForm.Page;
+ Model.Count = SearchForm.Count;
+ }
+
+ public class SearchFormModel
+ {
+ private string? _name;
+
+ public string? Name
+ {
+ get
+ {
+ return string.IsNullOrWhiteSpace(_name) ? null : _name;
+ }
+ set
+ {
+ _name = value;
+ }
+ }
+
+ private string? _email;
+
+ [EmailAddress]
+ public string? Email
+ {
+ get
+ {
+ return string.IsNullOrWhiteSpace(_email) ? null : _email;
+ }
+ set
+ {
+ _email = value;
+ }
+ }
+
+ public bool? Paid { get; set; }
+
+ public int Page { get; set; } = 1;
+
+ public int Count { get; set; } = 25;
+ }
+
+ public class ViewModel : PagedModel
+ {
+ public string Action { get; set; }
+ public bool SelfHosted { get; set; }
+ }
+}
+
diff --git a/src/Admin/AdminConsole/Components/_Imports.razor b/src/Admin/AdminConsole/Components/_Imports.razor
new file mode 100644
index 0000000000..7b48dcecad
--- /dev/null
+++ b/src/Admin/AdminConsole/Components/_Imports.razor
@@ -0,0 +1,3 @@
+@attribute [Authorize]
+
+@using Bit.Admin.Components
diff --git a/src/Admin/Components/BitPage.razor b/src/Admin/Components/BitPage.razor
new file mode 100644
index 0000000000..91690f96d3
--- /dev/null
+++ b/src/Admin/Components/BitPage.razor
@@ -0,0 +1,4 @@
+@Title
+
+@Title
+@ChildContent
diff --git a/src/Admin/Components/BitPage.razor.cs b/src/Admin/Components/BitPage.razor.cs
new file mode 100644
index 0000000000..29223bcc63
--- /dev/null
+++ b/src/Admin/Components/BitPage.razor.cs
@@ -0,0 +1,16 @@
+using Microsoft.AspNetCore.Components;
+
+namespace Bit.Admin.Components;
+
+public partial class BitPage : ComponentBase
+{
+ [Parameter]
+ public required string Title { get; set; }
+
+ [Parameter] public bool HideTitle { get; set; } = false;
+
+ [Parameter]
+ public required RenderFragment ChildContent { get; set; }
+
+ private string? TitleClass => HideTitle ? "sr-only" : null;
+}
diff --git a/src/Admin/Components/Navigation/NavDropDownItem.razor b/src/Admin/Components/Navigation/NavDropDownItem.razor
new file mode 100644
index 0000000000..a57604d33e
--- /dev/null
+++ b/src/Admin/Components/Navigation/NavDropDownItem.razor
@@ -0,0 +1,20 @@
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.Azure.Cosmos.Core.Collections
+
+
+ @Model.Label
+
+
+@code {
+ [Parameter]
+ public required ViewModel Model { get; set; }
+
+ public class ViewModel
+ {
+ public required string Label { get; set; }
+
+ public required string Link { get; set; }
+
+ public bool Show { get; set; } = true;
+ }
+}
diff --git a/src/Admin/Components/Navigation/NavItem.razor b/src/Admin/Components/Navigation/NavItem.razor
new file mode 100644
index 0000000000..d256be3595
--- /dev/null
+++ b/src/Admin/Components/Navigation/NavItem.razor
@@ -0,0 +1,61 @@
+@using Microsoft.AspNetCore.Components.Routing
+
+@inject NavigationManager NavigationManager
+
+
+ @if (Model.DropDownItems is { Count: > 0 })
+ {
+
+ Tools
+
+
+ }
+ else
+ {
+
+ @Model.Label
+
+ }
+
+
+@code {
+ public string Class { get; private set; }
+
+ [Parameter]
+ public ViewModel Model { get; set; }
+
+ protected override void OnInitialized()
+ {
+ var isActive = NavigationManager.Uri.ToLowerInvariant().Contains(Model.Link.ToLowerInvariant());
+ var classes = new List();
+ if (isActive)
+ {
+ classes.Add("active");
+ }
+ classes.Add("nav-item");
+ if (Model.DropDownItems is { Count: > 0 })
+ {
+ classes.Add("dropdown");
+ }
+ Class = string.Join(" ", classes);
+ }
+
+ public class ViewModel
+ {
+ public required string Link { get; set; }
+
+ public string LinkId { get; set; }
+
+ public required string Label { get; set; }
+
+ public HashSet? DropDownItems { get; set; }
+
+ public bool Show { get; set; } = true;
+ }
+}
diff --git a/src/Admin/Components/_Imports.razor b/src/Admin/Components/_Imports.razor
new file mode 100644
index 0000000000..66ebfa5d18
--- /dev/null
+++ b/src/Admin/Components/_Imports.razor
@@ -0,0 +1 @@
+@using Microsoft.AspNetCore.Components.Web
diff --git a/src/Admin/Startup.cs b/src/Admin/Startup.cs
index 9539b2ebe3..470bf65c58 100644
--- a/src/Admin/Startup.cs
+++ b/src/Admin/Startup.cs
@@ -170,7 +170,7 @@ public class Startup
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
- endpoints.MapRazorComponents();
+ endpoints.MapRazorComponents();
});
}
}