1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-27 07:42:15 -05:00
This commit is contained in:
Jonas Hendrickx 2024-10-21 08:24:44 +02:00
parent 1d3188d3f5
commit 420247a5e4
12 changed files with 513 additions and 1 deletions

View File

@ -18,6 +18,14 @@
<Folder Include="Billing\Controllers\" />
<Folder Include="Billing\Models\" />
</ItemGroup>
<ItemGroup>
<Compile Update="AdminConsole\Components\Admins.cs">
<DependentUpon>Admins.razor</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.10" />
</ItemGroup>
<Choose>
<When Condition="!$(DefineConstants.Contains('OSS'))">

View File

@ -0,0 +1,35 @@
@using Microsoft.AspNetCore.Components.Web
@inject IHttpContextAccessor HttpContextAccessor
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<base href="/"/>
<HeadOutlet/>
</head>
<body class="h-full bg-gray-50">
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<!-- The 'Resource' property will be used in 'HasAppHandler' to obtain the 'app' route parameter -->
<!-- If we implement new policies to protect resources, we should protect them with resource based uri and compare them with the user's claims. -->
<AuthorizeRouteView RouteData="@routeData" Resource="@HttpContextAccessor.HttpContext">
<!-- When visiting a page that requires authorization, but the user isn't authorized, redirect to the login page -->
<NotAuthorized>
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
</Found>
</Router>
<script src="_framework/blazor.web.js" autostart="false"></script>
<script>
Blazor.start({
ssr: { disableDomPreservation: true }
});
</script>
</body>
</html>

View File

@ -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; }
}

View File

@ -0,0 +1,61 @@
@using Bit.Core.AdminConsole.Enums.Provider
@inject Services.IAccessControlService AccessControlService
<h2>Provider Admins</h2>
<div class="row">
<div class="col-8">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th style="width: 190px;">Email</th>
<th style="width: 40px;">Status</th>
<th style="width: 30px;"></th>
</tr>
</thead>
<tbody>
@if(!Model.ProviderAdmins.Any())
{
<tr>
<td colspan="6">No results to list.</td>
</tr>
}
else
{
@foreach(var admin in Model.ProviderAdmins)
{
<tr>
<td class="align-middle">
@admin.Email
</td>
<td class="align-middle">
@admin.Status
</td>
<td>
@if(admin.Status.Equals(ProviderUserStatusType.Confirmed)
&& Model.Provider.Status.Equals(ProviderStatusType.Pending)
&& _canResendEmailInvite)
{
<!--@if(@TempData["InviteResentTo"] != null && @TempData["InviteResentTo"].ToString() == @admin.UserId.Value.ToString())
{
<button class="btn btn-outline-success btn-sm disabled" disabled>Invite Resent!</button>
}
else
{
<a class="btn btn-outline-secondary btn-sm"
data-id="@admin.Id" asp-controller="Providers"
asp-action="ResendInvite" asp-route-ownerId="@admin.UserId"
asp-route-providerId="@Model.Provider.Id">
Resend Setup Invite
</a>
}-->
}
</td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
</div>

View File

@ -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);
}
<h1>Provider <small>@Model.Provider.DisplayName()</small></h1>
<h2>Provider Information</h2>
<ViewInformation Model="@ViewModel" />
<Admins Model="@ViewModel" />
<form method="post" id="edit-form">
<div asp-validation-summary="All" class="alert alert-danger"></div>
<input type="hidden" asp-for="Type" readonly>
<h2>General</h2>
<dl class="row">
<dt class="col-sm-4 col-lg-3">Name</dt>
<dd class="col-sm-8 col-lg-9">@Model.Provider.DisplayName()</dd>
</dl>
<h2>Business Information</h2>
<dl class="row">
<dt class="col-sm-4 col-lg-3">Business Name</dt>
<dd class="col-sm-8 col-lg-9">@Model.Provider.DisplayBusinessName()</dd>
</dl>
<h2>Billing</h2>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="BillingEmail"></label>
<input type="email" class="form-control" asp-for="BillingEmail" readonly='@(!canEdit)'>
</div>
</div>
</div>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="BillingPhone"></label>
<input type="tel" class="form-control" asp-for="BillingPhone">
</div>
</div>
</div>
@if (featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && Model.Provider.IsBillable())
{
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="TeamsMonthlySeatMinimum"></label>
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
</div>
</div>
</div>
<div class="row">
<div class="col-sm">
<div class="form-group">
<div class="form-group">
<label asp-for="Gateway"></label>
<select class="form-control" asp-for="Gateway" asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
<option value="">--</option>
</select>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="GatewayCustomerId"></label>
<div class="input-group">
<input type="text" class="form-control" asp-for="GatewayCustomerId">
<div class="input-group-append">
<a href="@Model.GatewayCustomerUrl" class="btn btn-secondary" target="_blank">
<i class="fa fa-external-link"></i>
</a>
</div>
</div>
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="GatewaySubscriptionId"></label>
<div class="input-group">
<input type="text" class="form-control" asp-for="GatewaySubscriptionId">
<div class="input-group-append">
<a href="@Model.GatewaySubscriptionUrl" class="btn btn-secondary" target="_blank">
<i class="fa fa-external-link"></i>
</a>
</div>
</div>
</div>
</div>
</div>
}
</form>
<!--@await Html.PartialAsync("Organizations", Model)-->
@if (canEdit)
{
<!-- Modals -->
<div class="modal fade rounded" id="requestDeletionModal" tabindex="-1" aria-labelledby="requestDeletionModal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content rounded">
<div class="p-3">
<h4 class="font-weight-bolder" id="exampleModalLabel">Request provider deletion</h4>
</div>
<div class="modal-body">
<span class="font-weight-light">
Enter the email of the provider admin that will receive the request to delete the provider portal.
</span>
<form>
<div class="form-group">
<label for="provider-email" class="col-form-label">Provider email</label>
<input type="email" class="form-control" id="provider-email">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-primary btn-pill" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger btn-pill" onclick="initiateDeleteProvider('@Model.Provider.Id')">Send email request</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="DeleteModal" tabindex="-1" aria-labelledby="DeleteModal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content rounded">
<div class="p-3">
<h4 class="font-weight-bolder" id="exampleModalLabel">Delete provider</h4>
</div>
<div class="modal-body">
<span class="font-weight-light">
This action is permanent and irreversible. Enter the provider name to complete deletion of the provider and associated data.
</span>
<form>
<div class="form-group">
<label for="provider-name" class="col-form-label">Provider name</label>
<input type="text" class="form-control" id="provider-name">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-primary btn-pill" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger btn-pill" onclick="deleteProvider('@Model.Provider.Id');">Delete provider</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="linkedWarningModal" tabindex="-1" role="dialog" aria-labelledby="linkedWarningModal" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content rounded">
<div class="modal-body">
<h4 class="font-weight-bolder">Cannot Delete @Model.Name</h4>
<p class="font-weight-lighter">You must unlink all clients before you can delete @Model.Name.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-primary btn-pill" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary btn-pill" data-dismiss="modal">Ok</button>
</div>
</div>
</div>
</div>
<!-- End of Modal Section -->
<div class="d-flex mt-4">
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
<div class="ml-auto d-flex">
<button class="btn btn-danger" onclick="openRequestDeleteModal(@Model.ProviderOrganizations.Count())">Request Delete</button>
<button id="requestDeletionBtn" hidden="hidden" data-toggle="modal" data-target="#requestDeletionModal"></button>
<button class="btn btn-outline-danger ml-2" onclick="openDeleteModal(@Model.ProviderOrganizations.Count())">Delete</button>
<button id="deleteBtn" hidden="hidden" data-toggle="modal" data-target="#DeleteModal"></button>
<button id="linkAccWarningBtn" hidden="hidden" data-toggle="modal" data-target="#linkedWarningModal"></button>
</div>
</div>
}

View File

@ -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<ProviderPlan>());
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
};
}
}

View File

@ -0,0 +1,22 @@
@using Bit.SharedWeb.Utilities
@using Bit.Core.AdminConsole.Enums.Provider
<dl class="row">
<dt class="col-sm-4 col-lg-3">Id</dt>
<dd class="col-sm-8 col-lg-9"><code>@Model.Provider.Id</code></dd>
<dt class="col-sm-4 col-lg-3">Status</dt>
<dd class="col-sm-8 col-lg-9">@Model.Provider.Status</dd>
<dt class="col-sm-4 col-lg-3">Users</dt>
<dd class="col-sm-8 col-lg-9">@(Model.Provider.Type == ProviderType.Reseller ? "N/A" : Model.UserCount)</dd>
<dt class="col-sm-4 col-lg-3">Provider Type</dt>
<dd class="col-sm-8 col-lg-9">@(Model.Provider.Type.GetDisplayAttribute()?.GetName())</dd>
<dt class="col-sm-4 col-lg-3">Created</dt>
<dd class="col-sm-8 col-lg-9">@Model.Provider.CreationDate</dd>
<dt class="col-sm-4 col-lg-3">Modified</dt>
<dd class="col-sm-8 col-lg-9">@Model.Provider.RevisionDate</dd>
</dl>

View File

@ -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; }
}

View File

@ -0,0 +1,5 @@
@using Bit.Core.Utilities
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
@attribute [SelfHosted(NotSelfHostedOnly = true)]

View File

@ -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<IdentityOptions> options)
: RevalidatingServerAuthenticationStateProvider(loggerFactory)
{
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
protected override async Task<bool> 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<UserManager<User>>();
return await ValidateSecurityStampAsync(userManager, authenticationState.User);
}
private async Task<bool> ValidateSecurityStampAsync(UserManager<User> 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;
}
}
}

View File

@ -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

View File

@ -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<HostedServices.AzureQueueMailHostedService>();
}
}
services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
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<AdminConsoleRootApp>();
});
}
}