diff --git a/src/Admin/Admin.csproj b/src/Admin/Admin.csproj index 5493e65afd..20508c1c92 100644 --- a/src/Admin/Admin.csproj +++ b/src/Admin/Admin.csproj @@ -18,6 +18,14 @@ + + + Admins.razor + + + + + diff --git a/src/Admin/AdminConsole/AdminConsoleRootApp.razor b/src/Admin/AdminConsole/AdminConsoleRootApp.razor new file mode 100644 index 0000000000..de1fce1f57 --- /dev/null +++ b/src/Admin/AdminConsole/AdminConsoleRootApp.razor @@ -0,0 +1,35 @@ +@using Microsoft.AspNetCore.Components.Web +@inject IHttpContextAccessor HttpContextAccessor + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Admin/AdminConsole/Components/Admins.cs b/src/Admin/AdminConsole/Components/Admins.cs new file mode 100644 index 0000000000..a075938652 --- /dev/null +++ b/src/Admin/AdminConsole/Components/Admins.cs @@ -0,0 +1,14 @@ +using Bit.Admin.AdminConsole.Models; +using Bit.Admin.Enums; +using Bit.Admin.Services; +using Microsoft.AspNetCore.Components; + +namespace Bit.Admin.AdminConsole.Components; + +public partial class Admins( + IAccessControlService accessControlService) : ComponentBase +{ + private readonly bool _canResendEmailInvite = accessControlService.UserHasPermission(Permission.Provider_ResendEmailInvite); + + [Parameter] public ProviderViewModel Model { get; set; } +} diff --git a/src/Admin/AdminConsole/Components/Admins.razor b/src/Admin/AdminConsole/Components/Admins.razor new file mode 100644 index 0000000000..0993d3a218 --- /dev/null +++ b/src/Admin/AdminConsole/Components/Admins.razor @@ -0,0 +1,61 @@ +@using Bit.Core.AdminConsole.Enums.Provider +@inject Services.IAccessControlService AccessControlService + +

Provider Admins

+
+
+
+ + + + + + + + + + @if(!Model.ProviderAdmins.Any()) + { + + + + } + else + { + @foreach(var admin in Model.ProviderAdmins) + { + + + + + + } + } + +
EmailStatus
No results to list.
+ @admin.Email + + @admin.Status + + @if(admin.Status.Equals(ProviderUserStatusType.Confirmed) + && Model.Provider.Status.Equals(ProviderStatusType.Pending) + && _canResendEmailInvite) + { + + } +
+
+
+
diff --git a/src/Admin/AdminConsole/Components/EditProviderPage.razor b/src/Admin/AdminConsole/Components/EditProviderPage.razor new file mode 100644 index 0000000000..47ddf1513b --- /dev/null +++ b/src/Admin/AdminConsole/Components/EditProviderPage.razor @@ -0,0 +1,187 @@ +@page "/providers2/edit/{id:guid}" + +@using Bit.Admin.Enums +@using Bit.Core +@using Bit.Core.Billing.Extensions + +@{ + //ViewData["Title"] = "Provider: " + Model.Provider.DisplayName(); + var canEdit = accessControlService.UserHasPermission(Permission.Provider_Edit); +} + +

Provider @Model.Provider.DisplayName()

+ +

Provider Information

+ + +
+
+ +

General

+
+
Name
+
@Model.Provider.DisplayName()
+
+

Business Information

+
+
Business Name
+
@Model.Provider.DisplayBusinessName()
+
+

Billing

+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+ @if (featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && Model.Provider.IsBillable()) + { +
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+ +
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ + + +
+
+
+
+
+ } +
+ +@if (canEdit) +{ + + + + + + + +
+ +
+ + + + + + + + +
+
+} diff --git a/src/Admin/AdminConsole/Components/EditProviderPage.razor.cs b/src/Admin/AdminConsole/Components/EditProviderPage.razor.cs new file mode 100644 index 0000000000..52c2966a61 --- /dev/null +++ b/src/Admin/AdminConsole/Components/EditProviderPage.razor.cs @@ -0,0 +1,98 @@ +using Bit.Admin.AdminConsole.Models; +using Bit.Admin.Services; +using Bit.Admin.Utilities; +using Bit.Core; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Repositories; +using Bit.Core.Enums; +using Bit.Core.Services; +using Bit.Core.Settings; +using Microsoft.AspNetCore.Components; + +namespace Bit.Admin.AdminConsole.Components; + +public partial class EditProviderPage( + GlobalSettings globalSettings, + IAccessControlService accessControlService, + IFeatureService featureService, + IProviderRepository providerRepository, + IProviderOrganizationRepository providerOrganizationRepository, + IProviderPlanRepository providerPlanRepository, + IProviderUserRepository providerUserRepository, + IWebHostEnvironment webHostEnvironment) + : ComponentBase +{ + private readonly string _stripeUrl = webHostEnvironment.GetStripeUrl(); + private readonly string _braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl(); + private readonly string _braintreeMerchantId = globalSettings.Braintree.MerchantId; + + [Parameter] public Guid Id { get; set; } + + public ProviderEditModel? Model { get; set; } + + public ProviderViewModel? ViewModel { get; set; } + + protected override async Task OnInitializedAsync() + { + var provider = await providerRepository.GetByIdAsync(Id); + + if (provider == null) + { + return; + } + + var users = await providerUserRepository.GetManyDetailsByProviderAsync(Id); + var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(Id); + + var isConsolidatedBillingEnabled = featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling); + + if (!isConsolidatedBillingEnabled || !provider.IsBillable()) + { + Model = new ProviderEditModel(provider, users, providerOrganizations, new List()); + ViewModel = new ProviderViewModel(provider, users, providerOrganizations); + return; + } + + var providerPlans = await providerPlanRepository.GetByProviderId(Id); + + Model = new ProviderEditModel( + provider, users, providerOrganizations, + providerPlans.ToList(), GetGatewayCustomerUrl(provider), GetGatewaySubscriptionUrl(provider)); + + ViewModel = new ProviderViewModel(provider, users, providerOrganizations); + } + + private string GetGatewayCustomerUrl(Provider provider) + { + if (!provider.Gateway.HasValue || string.IsNullOrEmpty(provider.GatewayCustomerId)) + { + return null; + } + + return provider.Gateway switch + { + GatewayType.Stripe => $"{_stripeUrl}/customers/{provider.GatewayCustomerId}", + GatewayType.PayPal => $"{_braintreeMerchantUrl}/{_braintreeMerchantId}/${provider.GatewayCustomerId}", + _ => null + }; + } + + private string GetGatewaySubscriptionUrl(Provider provider) + { + if (!provider.Gateway.HasValue || string.IsNullOrEmpty(provider.GatewaySubscriptionId)) + { + return null; + } + + return provider.Gateway switch + { + GatewayType.Stripe => $"{_stripeUrl}/subscriptions/{provider.GatewaySubscriptionId}", + GatewayType.PayPal => $"{_braintreeMerchantUrl}/{_braintreeMerchantId}/subscriptions/${provider.GatewaySubscriptionId}", + _ => null + }; + } +} + diff --git a/src/Admin/AdminConsole/Components/ViewInformation.razor b/src/Admin/AdminConsole/Components/ViewInformation.razor new file mode 100644 index 0000000000..2f4298f2f6 --- /dev/null +++ b/src/Admin/AdminConsole/Components/ViewInformation.razor @@ -0,0 +1,22 @@ +@using Bit.SharedWeb.Utilities +@using Bit.Core.AdminConsole.Enums.Provider + +
+
Id
+
@Model.Provider.Id
+ +
Status
+
@Model.Provider.Status
+ +
Users
+
@(Model.Provider.Type == ProviderType.Reseller ? "N/A" : Model.UserCount)
+ +
Provider Type
+
@(Model.Provider.Type.GetDisplayAttribute()?.GetName())
+ +
Created
+
@Model.Provider.CreationDate
+ +
Modified
+
@Model.Provider.RevisionDate
+
diff --git a/src/Admin/AdminConsole/Components/ViewInformation.razor.cs b/src/Admin/AdminConsole/Components/ViewInformation.razor.cs new file mode 100644 index 0000000000..7f93beace3 --- /dev/null +++ b/src/Admin/AdminConsole/Components/ViewInformation.razor.cs @@ -0,0 +1,9 @@ +using Bit.Admin.AdminConsole.Models; +using Microsoft.AspNetCore.Components; + +namespace Bit.Admin.AdminConsole.Components; + +public partial class ViewInformation : ComponentBase +{ + [Parameter] public ProviderViewModel Model { get; set; } +} diff --git a/src/Admin/AdminConsole/Components/_Imports.razor b/src/Admin/AdminConsole/Components/_Imports.razor new file mode 100644 index 0000000000..97859fa7bf --- /dev/null +++ b/src/Admin/AdminConsole/Components/_Imports.razor @@ -0,0 +1,5 @@ +@using Bit.Core.Utilities +@using Microsoft.AspNetCore.Authorization + +@attribute [Authorize] +@attribute [SelfHosted(NotSelfHostedOnly = true)] diff --git a/src/Admin/AdminConsole/IdentityRevalidatingAuthenticationStateProvider.cs b/src/Admin/AdminConsole/IdentityRevalidatingAuthenticationStateProvider.cs new file mode 100644 index 0000000000..15b367aa97 --- /dev/null +++ b/src/Admin/AdminConsole/IdentityRevalidatingAuthenticationStateProvider.cs @@ -0,0 +1,48 @@ +using System.Security.Claims; +using Bit.Core.Entities; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Server; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; + +namespace Bit.Admin.AdminConsole; + +// This is a server-side AuthenticationStateProvider that revalidates the security stamp for the connected user +// every 30 minutes an interactive circuit is connected. +// Used for Blazor experiment +internal sealed class IdentityRevalidatingAuthenticationStateProvider( + ILoggerFactory loggerFactory, + IServiceScopeFactory scopeFactory, + IOptions options) + : RevalidatingServerAuthenticationStateProvider(loggerFactory) +{ + protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30); + + protected override async Task ValidateAuthenticationStateAsync( + AuthenticationState authenticationState, CancellationToken cancellationToken) + { + // Get the user manager from a new scope to ensure it fetches fresh data + await using var scope = scopeFactory.CreateAsyncScope(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + return await ValidateSecurityStampAsync(userManager, authenticationState.User); + } + + private async Task ValidateSecurityStampAsync(UserManager userManager, ClaimsPrincipal principal) + { + var user = await userManager.GetUserAsync(principal); + if (user is null) + { + return false; + } + else if (!userManager.SupportsUserSecurityStamp) + { + return true; + } + else + { + var principalStamp = principal.FindFirstValue(options.Value.ClaimsIdentity.SecurityStampClaimType); + var userStamp = await userManager.GetSecurityStampAsync(user); + return principalStamp == userStamp; + } + } +} diff --git a/src/Admin/AdminConsole/_Imports.razor b/src/Admin/AdminConsole/_Imports.razor new file mode 100644 index 0000000000..d50bf8b980 --- /dev/null +++ b/src/Admin/AdminConsole/_Imports.razor @@ -0,0 +1,15 @@ +@attribute [Authorize] + +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web + +@using Microsoft.AspNetCore.Mvc.ViewFeatures +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using System.Net.Http +@using System.Net.Http.Json +@using System.Text.Json diff --git a/src/Admin/Startup.cs b/src/Admin/Startup.cs index 11f9e7ce68..9539b2ebe3 100644 --- a/src/Admin/Startup.cs +++ b/src/Admin/Startup.cs @@ -1,4 +1,5 @@ using System.Globalization; +using Bit.Admin.AdminConsole; using Bit.Admin.IdentityServer; using Bit.Core.Context; using Bit.Core.Settings; @@ -11,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Bit.Admin.Services; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Migration; +using Microsoft.AspNetCore.Components.Authorization; #if !OSS using Bit.Commercial.Core.Utilities; @@ -129,6 +131,9 @@ public class Startup services.AddHostedService(); } } + + services.AddScoped(); + services.AddRazorComponents(); } public void Configure( @@ -161,6 +166,11 @@ public class Startup app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); - app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute()); + app.UseAntiforgery(); + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + endpoints.MapRazorComponents(); + }); } }