From fe1ffb6a224f04daea172f3b2acbaed3d062e51f Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Thu, 3 Jun 2021 18:58:29 +0200 Subject: [PATCH] [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 --- src/Admin/Controllers/ProvidersController.cs | 118 ++++++ src/Admin/Models/CreateProviderModel.cs | 13 + src/Admin/Models/ProviderEditModel.cs | 29 ++ src/Admin/Models/ProviderViewModel.cs | 27 ++ src/Admin/Models/ProvidersModel.cs | 14 + src/Admin/Views/Providers/Create.cshtml | 17 + src/Admin/Views/Providers/Edit.cshtml | 51 +++ src/Admin/Views/Providers/Index.cshtml | 91 ++++ src/Admin/Views/Providers/View.cshtml | 13 + .../Views/Providers/_ViewInformation.cshtml | 20 + src/Admin/Views/Shared/_Layout.cshtml | 3 + src/Core/Enums/EventType.cs | 5 + .../ProviderOrganizationProviderUserType.cs | 8 + src/Core/Enums/Provider/ProviderStatusType.cs | 8 + .../Enums/Provider/ProviderUserStatusType.cs | 9 + src/Core/Enums/Provider/ProviderUserType.cs | 8 + .../Provider/ProviderSetupInvite.html.hbs | 16 + .../Provider/ProviderSetupInvite.text.hbs | 5 + .../Provider/ProviderUserConfirmed.html.hbs | 14 + .../Provider/ProviderUserConfirmed.text.hbs | 5 + .../Provider/ProviderUserInvited.html.hbs | 21 + .../Provider/ProviderUserInvited.text.hbs | 7 + .../Business/Provider/ProviderUserInvite.cs | 15 + .../Provider/ProviderSetupInviteViewModel.cs | 14 + .../ProviderUserConfirmedViewModel.cs | 7 + .../Provider/ProviderUserInvitedViewModel.cs | 20 + src/Core/Models/Table/Provider/Provider.cs | 31 ++ .../Table/Provider/ProviderOrganization.cs | 24 ++ .../ProviderOrganizationProviderUser.cs | 25 ++ .../Models/Table/Provider/ProviderUser.cs | 28 ++ ...viderOrganizationProviderUserRepository.cs | 9 + .../IProviderOrganizationRepository.cs | 9 + src/Core/Repositories/IProviderRepository.cs | 12 + .../Repositories/IProviderUserRepository.cs | 16 + ...viderOrganizationProviderUserRepository.cs | 17 + .../ProviderOrganizationRepository.cs | 17 + .../SqlServer/ProviderRepository.cs | 38 ++ .../SqlServer/ProviderUserRepository.cs | 73 ++++ src/Core/Services/IEventService.cs | 4 + src/Core/Services/IMailService.cs | 4 + src/Core/Services/IProviderService.cs | 31 ++ .../Services/Implementations/EventService.cs | 7 + .../Implementations/HandlebarsMailService.cs | 50 +++ .../Implementations/ProviderService.cs | 348 ++++++++++++++++ .../NoopImplementations/NoopEventService.cs | 11 + .../NoopImplementations/NoopMailService.cs | 18 +- .../Utilities/ServiceCollectionExtensions.cs | 7 +- src/Sql/Sql.sqlproj | 5 + .../ProviderUser_DeleteByIds.sql | 42 ++ .../ProviderUser_ReadByIds.sql | 18 + .../ProviderUser_ReadByProviderId.sql | 4 +- ...roviderUser_ReadCountByProviderIdEmail.sql | 21 + .../dbo/Stored Procedures/Provider_Search.sql | 41 ++ ...mpAccountRevisionDateByProviderUserIds.sql | 18 + src/Sql/dbo/Tables/Provider.sql | 4 +- .../AutoFixture/ProviderUserFixtures.cs | 45 ++ .../Services/ProviderServiceTests.cs | 392 ++++++++++++++++++ ...rovider.sql => 2021-05-26_00_Provider.sql} | 189 ++++++++- 58 files changed, 2110 insertions(+), 6 deletions(-) create mode 100644 src/Admin/Controllers/ProvidersController.cs create mode 100644 src/Admin/Models/CreateProviderModel.cs create mode 100644 src/Admin/Models/ProviderEditModel.cs create mode 100644 src/Admin/Models/ProviderViewModel.cs create mode 100644 src/Admin/Models/ProvidersModel.cs create mode 100644 src/Admin/Views/Providers/Create.cshtml create mode 100644 src/Admin/Views/Providers/Edit.cshtml create mode 100644 src/Admin/Views/Providers/Index.cshtml create mode 100644 src/Admin/Views/Providers/View.cshtml create mode 100644 src/Admin/Views/Providers/_ViewInformation.cshtml create mode 100644 src/Core/Enums/Provider/ProviderOrganizationProviderUserType.cs create mode 100644 src/Core/Enums/Provider/ProviderStatusType.cs create mode 100644 src/Core/Enums/Provider/ProviderUserStatusType.cs create mode 100644 src/Core/Enums/Provider/ProviderUserType.cs create mode 100644 src/Core/MailTemplates/Handlebars/Provider/ProviderSetupInvite.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/Provider/ProviderSetupInvite.text.hbs create mode 100644 src/Core/MailTemplates/Handlebars/Provider/ProviderUserConfirmed.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/Provider/ProviderUserConfirmed.text.hbs create mode 100644 src/Core/MailTemplates/Handlebars/Provider/ProviderUserInvited.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/Provider/ProviderUserInvited.text.hbs create mode 100644 src/Core/Models/Business/Provider/ProviderUserInvite.cs create mode 100644 src/Core/Models/Mail/Provider/ProviderSetupInviteViewModel.cs create mode 100644 src/Core/Models/Mail/Provider/ProviderUserConfirmedViewModel.cs create mode 100644 src/Core/Models/Mail/Provider/ProviderUserInvitedViewModel.cs create mode 100644 src/Core/Models/Table/Provider/Provider.cs create mode 100644 src/Core/Models/Table/Provider/ProviderOrganization.cs create mode 100644 src/Core/Models/Table/Provider/ProviderOrganizationProviderUser.cs create mode 100644 src/Core/Models/Table/Provider/ProviderUser.cs create mode 100644 src/Core/Repositories/IProviderOrganizationProviderUserRepository.cs create mode 100644 src/Core/Repositories/IProviderOrganizationRepository.cs create mode 100644 src/Core/Repositories/IProviderRepository.cs create mode 100644 src/Core/Repositories/IProviderUserRepository.cs create mode 100644 src/Core/Repositories/SqlServer/ProviderOrganizationProviderUserRepository.cs create mode 100644 src/Core/Repositories/SqlServer/ProviderOrganizationRepository.cs create mode 100644 src/Core/Repositories/SqlServer/ProviderRepository.cs create mode 100644 src/Core/Repositories/SqlServer/ProviderUserRepository.cs create mode 100644 src/Core/Services/IProviderService.cs create mode 100644 src/Core/Services/Implementations/ProviderService.cs create mode 100644 src/Sql/dbo/Stored Procedures/ProviderUser_DeleteByIds.sql create mode 100644 src/Sql/dbo/Stored Procedures/ProviderUser_ReadByIds.sql create mode 100644 src/Sql/dbo/Stored Procedures/ProviderUser_ReadCountByProviderIdEmail.sql create mode 100644 src/Sql/dbo/Stored Procedures/Provider_Search.sql create mode 100644 src/Sql/dbo/Stored Procedures/User_BumpAccountRevisionDateByProviderUserIds.sql create mode 100644 test/Core.Test/AutoFixture/ProviderUserFixtures.cs create mode 100644 test/Core.Test/Services/ProviderServiceTests.cs rename util/Migrator/DbScripts/{2021-05-14_00_Provider.sql => 2021-05-26_00_Provider.sql} (80%) diff --git a/src/Admin/Controllers/ProvidersController.cs b/src/Admin/Controllers/ProvidersController.cs new file mode 100644 index 0000000000..b97f50a9f1 --- /dev/null +++ b/src/Admin/Controllers/ProvidersController.cs @@ -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 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, + 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 Create(CreateProviderModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + await _providerService.CreateAsync(model.OwnerEmail); + + return RedirectToAction("Index"); + } + + public async Task 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 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 Delete(Guid id) + { + var provider = await _providerRepository.GetByIdAsync(id); + if (provider != null) + { + await _providerRepository.DeleteAsync(provider); + } + + return RedirectToAction("Index"); + } + } +} diff --git a/src/Admin/Models/CreateProviderModel.cs b/src/Admin/Models/CreateProviderModel.cs new file mode 100644 index 0000000000..ad4c547f42 --- /dev/null +++ b/src/Admin/Models/CreateProviderModel.cs @@ -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; } + } +} diff --git a/src/Admin/Models/ProviderEditModel.cs b/src/Admin/Models/ProviderEditModel.cs new file mode 100644 index 0000000000..652234e845 --- /dev/null +++ b/src/Admin/Models/ProviderEditModel.cs @@ -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 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; } + } +} diff --git a/src/Admin/Models/ProviderViewModel.cs b/src/Admin/Models/ProviderViewModel.cs new file mode 100644 index 0000000000..9f77e41971 --- /dev/null +++ b/src/Admin/Models/ProviderViewModel.cs @@ -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 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; } + } +} diff --git a/src/Admin/Models/ProvidersModel.cs b/src/Admin/Models/ProvidersModel.cs new file mode 100644 index 0000000000..dc2d1e87be --- /dev/null +++ b/src/Admin/Models/ProvidersModel.cs @@ -0,0 +1,14 @@ +using Bit.Core.Models.Table; +using Bit.Core.Models.Table.Provider; + +namespace Bit.Admin.Models +{ + public class ProvidersModel : PagedModel + { + 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; } + } +} diff --git a/src/Admin/Views/Providers/Create.cshtml b/src/Admin/Views/Providers/Create.cshtml new file mode 100644 index 0000000000..0369ca0179 --- /dev/null +++ b/src/Admin/Views/Providers/Create.cshtml @@ -0,0 +1,17 @@ +@model CreateProviderModel +@{ + ViewData["Title"] = "Create Provider"; +} + +

Create Provider

+ +
+
+ +
+ + +
+ + +
diff --git a/src/Admin/Views/Providers/Edit.cshtml b/src/Admin/Views/Providers/Edit.cshtml new file mode 100644 index 0000000000..2e75614aaa --- /dev/null +++ b/src/Admin/Views/Providers/Edit.cshtml @@ -0,0 +1,51 @@ +@model ProviderEditModel +@{ + ViewData["Title"] = "Provider: " + Model.Provider.Name; +} + +

Provider @Model.Provider.Name

+ +

Provider Information

+@await Html.PartialAsync("_ViewInformation", Model) +
+

General

+
+
+
+ + +
+
+
+
+ + +
+

Business Information

+
+
+
+ + +
+
+
+

Billing

+
+
+
+ + +
+
+
+
+
+ +
+
+ +
+
+
diff --git a/src/Admin/Views/Providers/Index.cshtml b/src/Admin/Views/Providers/Index.cshtml new file mode 100644 index 0000000000..b1d9a7a6b2 --- /dev/null +++ b/src/Admin/Views/Providers/Index.cshtml @@ -0,0 +1,91 @@ +@model ProvidersModel +@{ + ViewData["Title"] = "Providers"; +} + +

Providers

+ +
+
+
+ + + + + +
+
+ +
+ +
+ + + + + + + + + + @if(!Model.Items.Any()) + { + + + + } + else + { + @foreach(var provider in Model.Items) + { + + + + + + } + } + +
NameStatusCreated
No results to list.
+ @(provider.Name ?? "Pending") + @provider.Status + + @provider.CreationDate.ToShortDateString() + +
+
+ + diff --git a/src/Admin/Views/Providers/View.cshtml b/src/Admin/Views/Providers/View.cshtml new file mode 100644 index 0000000000..14bb9ed6cd --- /dev/null +++ b/src/Admin/Views/Providers/View.cshtml @@ -0,0 +1,13 @@ +@model ProviderViewModel +@{ + ViewData["Title"] = "Provider: " + Model.Provider.Name; +} + +

Provider @Model.Provider.Name

+ +

Information

+@await Html.PartialAsync("_ViewInformation", Model) +
+ +
diff --git a/src/Admin/Views/Providers/_ViewInformation.cshtml b/src/Admin/Views/Providers/_ViewInformation.cshtml new file mode 100644 index 0000000000..89e5f5aced --- /dev/null +++ b/src/Admin/Views/Providers/_ViewInformation.cshtml @@ -0,0 +1,20 @@ +@model ProviderViewModel +
+
Id
+
@Model.Provider.Id
+ +
Status
+
@Model.Provider.Status
+ +
Users
+
@Model.UserCount
+ +
ProviderAdmins
+
@(string.IsNullOrWhiteSpace(Model.ProviderAdmins) ? "None" : Model.ProviderAdmins)
+ +
Created
+
@Model.Provider.CreationDate.ToString()
+ +
Modified
+
@Model.Provider.RevisionDate.ToString()
+
diff --git a/src/Admin/Views/Shared/_Layout.cshtml b/src/Admin/Views/Shared/_Layout.cshtml index 635ab4bc43..ef06018b4c 100644 --- a/src/Admin/Views/Shared/_Layout.cshtml +++ b/src/Admin/Views/Shared/_Layout.cshtml @@ -40,6 +40,9 @@ @if(!GlobalSettings.SelfHosted) { +