1
0
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:
Oscar Hinton
2021-06-03 18:58:29 +02:00
committed by GitHub
parent 58954f161e
commit fe1ffb6a22
58 changed files with 2110 additions and 6 deletions

View 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");
}
}
}

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

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

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

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

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

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

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

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

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

View File

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