mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 15:42:48 -05:00
[Provider] Server entities and models (#1370)
* Mock out provider models and service * Implement CreateAsync, CompleteSetupAsync, UpdateAsync, InviteUserAsync and ResendInvitesAsync * Implement AcceptUserAsync and ConfirmUsersAsync * Implement SaveUserAsync and DeleteUserAsync * Add email templates * Add admin operations for providers * Fix mail template names * Rename roles * Verify provider has provideradmin * Add self hosted check to admin controller * Resolve review comments * Update sql queries * Change create provider to use email instead of userId
This commit is contained in:
118
src/Admin/Controllers/ProvidersController.cs
Normal file
118
src/Admin/Controllers/ProvidersController.cs
Normal file
@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Admin.Models;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Admin.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public class ProvidersController : Controller
|
||||
{
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IProviderService _providerService;
|
||||
|
||||
public ProvidersController(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
|
||||
IProviderService providerService, GlobalSettings globalSettings)
|
||||
{
|
||||
_providerRepository = providerRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_providerService = providerService;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Index(string name = null, string userEmail = null, int page = 1, int count = 25)
|
||||
{
|
||||
if (page < 1)
|
||||
{
|
||||
page = 1;
|
||||
}
|
||||
|
||||
if (count < 1)
|
||||
{
|
||||
count = 1;
|
||||
}
|
||||
|
||||
var skip = (page - 1) * count;
|
||||
var providers = await _providerRepository.SearchAsync(name, userEmail, skip, count);
|
||||
return View(new ProvidersModel
|
||||
{
|
||||
Items = providers as List<Provider>,
|
||||
Name = string.IsNullOrWhiteSpace(name) ? null : name,
|
||||
UserEmail = string.IsNullOrWhiteSpace(userEmail) ? null : userEmail,
|
||||
Page = page,
|
||||
Count = count,
|
||||
Action = _globalSettings.SelfHosted ? "View" : "Edit",
|
||||
SelfHosted = _globalSettings.SelfHosted
|
||||
});
|
||||
}
|
||||
|
||||
public IActionResult Create(string ownerEmail = null)
|
||||
{
|
||||
return View(new CreateProviderModel
|
||||
{
|
||||
OwnerEmail = ownerEmail
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create(CreateProviderModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
await _providerService.CreateAsync(model.OwnerEmail);
|
||||
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
public async Task<IActionResult> View(Guid id)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(id);
|
||||
if (provider == null)
|
||||
{
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
var users = await _providerUserRepository.GetManyByProviderAsync(id);
|
||||
return View(new ProviderViewModel(provider, users));
|
||||
}
|
||||
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<IActionResult> Edit(Guid id)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(id);
|
||||
if (provider == null)
|
||||
{
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
var users = await _providerUserRepository.GetManyByProviderAsync(id);
|
||||
return View(new ProviderEditModel(provider, users));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(id);
|
||||
if (provider != null)
|
||||
{
|
||||
await _providerRepository.DeleteAsync(provider);
|
||||
}
|
||||
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
}
|
||||
}
|
13
src/Admin/Models/CreateProviderModel.cs
Normal file
13
src/Admin/Models/CreateProviderModel.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Admin.Models
|
||||
{
|
||||
public class CreateProviderModel
|
||||
{
|
||||
public CreateProviderModel() { }
|
||||
|
||||
[Display(Name = "Owner Email")]
|
||||
[Required]
|
||||
public string OwnerEmail { get; set; }
|
||||
}
|
||||
}
|
29
src/Admin/Models/ProviderEditModel.cs
Normal file
29
src/Admin/Models/ProviderEditModel.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Bit.Core.Enums.Provider;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
|
||||
namespace Bit.Admin.Models
|
||||
{
|
||||
public class ProviderEditModel : ProviderViewModel
|
||||
{
|
||||
public ProviderEditModel(Provider provider, IEnumerable<ProviderUser> providerUsers)
|
||||
: base(provider, providerUsers)
|
||||
{
|
||||
Name = provider.Name;
|
||||
BusinessName = provider.BusinessName;
|
||||
BillingEmail = provider.BillingEmail;
|
||||
Enabled = provider.Enabled;
|
||||
}
|
||||
|
||||
public string Administrators { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
public string BillingEmail { get; set; }
|
||||
|
||||
public string BusinessName { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
27
src/Admin/Models/ProviderViewModel.cs
Normal file
27
src/Admin/Models/ProviderViewModel.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Bit.Core.Enums.Provider;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
|
||||
namespace Bit.Admin.Models
|
||||
{
|
||||
public class ProviderViewModel
|
||||
{
|
||||
public ProviderViewModel(Provider provider, IEnumerable<ProviderUser> providerUsers)
|
||||
{
|
||||
Provider = provider;
|
||||
UserCount = providerUsers.Count();
|
||||
|
||||
ProviderAdmins = string.Join(", ",
|
||||
providerUsers
|
||||
.Where(u => u.Type == ProviderUserType.ProviderAdmin && u.Status == ProviderUserStatusType.Confirmed)
|
||||
.Select(u => u.Email));
|
||||
}
|
||||
|
||||
public int UserCount { get; set; }
|
||||
|
||||
public Provider Provider { get; set; }
|
||||
|
||||
public string ProviderAdmins { get; set; }
|
||||
}
|
||||
}
|
14
src/Admin/Models/ProvidersModel.cs
Normal file
14
src/Admin/Models/ProvidersModel.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Models.Table.Provider;
|
||||
|
||||
namespace Bit.Admin.Models
|
||||
{
|
||||
public class ProvidersModel : PagedModel<Provider>
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string UserEmail { get; set; }
|
||||
public bool? Paid { get; set; }
|
||||
public string Action { get; set; }
|
||||
public bool SelfHosted { get; set; }
|
||||
}
|
||||
}
|
17
src/Admin/Views/Providers/Create.cshtml
Normal file
17
src/Admin/Views/Providers/Create.cshtml
Normal file
@ -0,0 +1,17 @@
|
||||
@model CreateProviderModel
|
||||
@{
|
||||
ViewData["Title"] = "Create Provider";
|
||||
}
|
||||
|
||||
<h1>Create Provider</h1>
|
||||
|
||||
<form method="post">
|
||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="OwnerEmail"></label>
|
||||
<input type="text" class="form-control" asp-for="OwnerEmail">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
||||
</form>
|
51
src/Admin/Views/Providers/Edit.cshtml
Normal file
51
src/Admin/Views/Providers/Edit.cshtml
Normal file
@ -0,0 +1,51 @@
|
||||
@model ProviderEditModel
|
||||
@{
|
||||
ViewData["Title"] = "Provider: " + Model.Provider.Name;
|
||||
}
|
||||
|
||||
<h1>Provider <small>@Model.Provider.Name</small></h1>
|
||||
|
||||
<h2>Provider Information</h2>
|
||||
@await Html.PartialAsync("_ViewInformation", Model)
|
||||
<form method="post" id="edit-form">
|
||||
<h2>General</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="Name"></label>
|
||||
<input type="text" class="form-control" asp-for="Name" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" class="form-check-input" asp-for="Enabled">
|
||||
<label class="form-check-label" asp-for="Enabled"></label>
|
||||
</div>
|
||||
<h2>Business Information</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="BusinessName"></label>
|
||||
<input type="text" class="form-control" asp-for="BusinessName">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="d-flex mt-4">
|
||||
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
|
||||
<div class="ml-auto d-flex">
|
||||
<form asp-action="Delete" asp-route-id="@Model.Provider.Id"
|
||||
onsubmit="return confirm('Are you sure you want to delete this provider (@Model.Provider.Name)?')">
|
||||
<button class="btn btn-danger" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
91
src/Admin/Views/Providers/Index.cshtml
Normal file
91
src/Admin/Views/Providers/Index.cshtml
Normal file
@ -0,0 +1,91 @@
|
||||
@model ProvidersModel
|
||||
@{
|
||||
ViewData["Title"] = "Providers";
|
||||
}
|
||||
|
||||
<h1>Providers</h1>
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col">
|
||||
<form class="form-inline mb-2" method="get">
|
||||
<label class="sr-only" asp-for="Name">Name</label>
|
||||
<input type="text" class="form-control mb-2 mr-2" placeholder="Name" asp-for="Name" name="name">
|
||||
<label class="sr-only" asp-for="UserEmail">User email</label>
|
||||
<input type="text" class="form-control mb-2 mr-2" placeholder="User email" asp-for="UserEmail" name="userEmail">
|
||||
<button type="submit" class="btn btn-primary mb-2" title="Search"><i class="fa fa-search"></i> Search</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a asp-action="Create" class="btn btn-secondary">Create Provider</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th style="width: 190px;">Status</th>
|
||||
<th style="width: 150px;">Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if(!Model.Items.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="5">No results to list.</td>
|
||||
</tr>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach(var provider in Model.Items)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<a asp-action="@Model.Action" asp-route-id="@provider.Id">@(provider.Name ?? "Pending")</a>
|
||||
</td>
|
||||
<td>@provider.Status</td>
|
||||
<td>
|
||||
<span title="@provider.CreationDate.ToString()">
|
||||
@provider.CreationDate.ToShortDateString()
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<ul class="pagination">
|
||||
@if(Model.PreviousPage.HasValue)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" asp-action="Index" asp-route-page="@Model.PreviousPage.Value"
|
||||
asp-route-count="@Model.Count" asp-route-userEmail="@Model.UserEmail"
|
||||
asp-route-name="@Model.Name" asp-route-paid="@Model.Paid">Previous</a>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
}
|
||||
@if(Model.NextPage.HasValue)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" asp-action="Index" asp-route-page="@Model.NextPage.Value"
|
||||
asp-route-count="@Model.Count" asp-route-userEmail="@Model.UserEmail"
|
||||
asp-route-name="@Model.Name" asp-route-paid="@Model.Paid">Next</a>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1">Next</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
13
src/Admin/Views/Providers/View.cshtml
Normal file
13
src/Admin/Views/Providers/View.cshtml
Normal file
@ -0,0 +1,13 @@
|
||||
@model ProviderViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Provider: " + Model.Provider.Name;
|
||||
}
|
||||
|
||||
<h1>Provider <small>@Model.Provider.Name</small></h1>
|
||||
|
||||
<h2>Information</h2>
|
||||
@await Html.PartialAsync("_ViewInformation", Model)
|
||||
<form asp-action="Delete" asp-route-id="@Model.Provider.Id"
|
||||
onsubmit="return confirm('Are you sure you want to delete this provider (@Model.Provider.Name)?')">
|
||||
<button class="btn btn-danger" type="submit">Delete</button>
|
||||
</form>
|
20
src/Admin/Views/Providers/_ViewInformation.cshtml
Normal file
20
src/Admin/Views/Providers/_ViewInformation.cshtml
Normal file
@ -0,0 +1,20 @@
|
||||
@model ProviderViewModel
|
||||
<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.UserCount</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">ProviderAdmins</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@(string.IsNullOrWhiteSpace(Model.ProviderAdmins) ? "None" : Model.ProviderAdmins)</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Created</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.Provider.CreationDate.ToString()</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Modified</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.Provider.RevisionDate.ToString()</dd>
|
||||
</dl>
|
@ -40,6 +40,9 @@
|
||||
</li>
|
||||
@if(!GlobalSettings.SelfHosted)
|
||||
{
|
||||
<li class="nav-item" active-controller="Providers">
|
||||
<a class="nav-link" asp-controller="Providers" asp-action="Index">Providers</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown" active-controller="tools">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="toolsDropdown" role="button"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
|
Reference in New Issue
Block a user