diff --git a/src/Admin/Controllers/OrganizationsController.cs b/src/Admin/Controllers/OrganizationsController.cs new file mode 100644 index 0000000000..ea0b38f133 --- /dev/null +++ b/src/Admin/Controllers/OrganizationsController.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Bit.Core.Repositories; +using System.Threading.Tasks; +using Bit.Admin.Models; +using System.Collections.Generic; +using Bit.Core.Models.Table; + +namespace Bit.Admin.Controllers +{ + [Authorize] + public class OrganizationsController : Controller + { + private readonly IOrganizationRepository _organizationRepository; + + public OrganizationsController(IOrganizationRepository organizationRepository) + { + _organizationRepository = organizationRepository; + } + + public async Task Index(string name = null, string userEmail = null, bool paid = false, + int page = 1, int count = 25) + { + if(page < 1) + { + page = 1; + } + + if(count < 1) + { + count = 1; + } + + var skip = (page - 1) * count; + var organizations = await _organizationRepository.SearchAsync(name, userEmail, paid ? (bool?)true : null, + skip, count); + return View(new OrganizationsModel + { + Items = organizations as List, + Name = string.IsNullOrWhiteSpace(name) ? null : name, + UserEmail = string.IsNullOrWhiteSpace(userEmail) ? null : userEmail, + Paid = paid, + Page = page, + Count = count + }); + } + } +} diff --git a/src/Admin/Controllers/UsersController.cs b/src/Admin/Controllers/UsersController.cs index 75bee22c7c..7ee29ee2fd 100644 --- a/src/Admin/Controllers/UsersController.cs +++ b/src/Admin/Controllers/UsersController.cs @@ -32,10 +32,10 @@ namespace Bit.Admin.Controllers } var skip = (page - 1) * count; - var users = await _userRepository.SearchByEmailAsync(email, skip, count); + var users = await _userRepository.SearchAsync(email, skip, count); return View(new UsersModel { - Users = users as List, + Items = users as List, Email = string.IsNullOrWhiteSpace(email) ? null : email, Page = page, Count = count diff --git a/src/Admin/Models/OrganizationsModel.cs b/src/Admin/Models/OrganizationsModel.cs new file mode 100644 index 0000000000..f03a065b2c --- /dev/null +++ b/src/Admin/Models/OrganizationsModel.cs @@ -0,0 +1,11 @@ +using Bit.Core.Models.Table; + +namespace Bit.Admin.Models +{ + public class OrganizationsModel : PagedModel + { + public string Name { get; set; } + public string UserEmail { get; set; } + public bool Paid { get; set; } + } +} diff --git a/src/Admin/Models/PagedModel.cs b/src/Admin/Models/PagedModel.cs new file mode 100644 index 0000000000..a4e4087ac2 --- /dev/null +++ b/src/Admin/Models/PagedModel.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Bit.Admin.Models +{ + public abstract class PagedModel + { + public List Items { get; set; } + public int Page { get; set; } + public int Count { get; set; } + public int? PreviousPage => Page < 2 ? (int?)null : Page - 1; + public int? NextPage => Items.Count < Count ? (int?)null : Page + 1; + } +} diff --git a/src/Admin/Models/UsersModel.cs b/src/Admin/Models/UsersModel.cs index 22ac845815..024a1001fa 100644 --- a/src/Admin/Models/UsersModel.cs +++ b/src/Admin/Models/UsersModel.cs @@ -1,16 +1,9 @@ -using System.Collections; -using System.Collections.Generic; -using Bit.Core.Models.Table; +using Bit.Core.Models.Table; namespace Bit.Admin.Models { - public class UsersModel + public class UsersModel : PagedModel { - public List Users { get; set; } public string Email { get; set; } - public int Page { get; set; } - public int Count { get; set; } - public int? PreviousPage => Page < 2 ? (int?)null : Page - 1; - public int? NextPage => Users.Count < Count ? (int?)null : Page + 1; } } diff --git a/src/Admin/Views/Organizations/Index.cshtml b/src/Admin/Views/Organizations/Index.cshtml new file mode 100644 index 0000000000..56b03beced --- /dev/null +++ b/src/Admin/Views/Organizations/Index.cshtml @@ -0,0 +1,124 @@ +@model OrganizationsModel +@{ + ViewData["Title"] = "Organizations"; +} + +

Organizations

+ +
+ + + + +
+ + +
+ +
+ +
+ + + + + + + + + + + + @if(!Model.Items.Any()) + { + + + + } + else + { + @foreach(var org in Model.Items) + { + + + + + + + + } + } + +
NamePlanSeatsCreatedDetails
No results to list.
+ @org.Name + + @org.Plan + + @org.Seats + + + @org.CreationDate.ToShortDateString() + + + @if(!string.IsNullOrWhiteSpace(org.GatewaySubscriptionId)) + { + + } + else + { + + } + @if(org.MaxStorageGb.HasValue && org.MaxStorageGb > 1) + { + + } + else + { + + } + @if(org.Enabled) + { + + } + else + { + + } +
+
+ + diff --git a/src/Admin/Views/Shared/_Layout.cshtml b/src/Admin/Views/Shared/_Layout.cshtml index 3cfe2a2ba0..a113e3726a 100644 --- a/src/Admin/Views/Shared/_Layout.cshtml +++ b/src/Admin/Views/Shared/_Layout.cshtml @@ -33,6 +33,9 @@ + diff --git a/src/Admin/Views/Users/Index.cshtml b/src/Admin/Views/Users/Index.cshtml index 3ec29ad29c..063f44b28d 100644 --- a/src/Admin/Views/Users/Index.cshtml +++ b/src/Admin/Views/Users/Index.cshtml @@ -7,12 +7,8 @@
-
- -
- -
-
+ +
@@ -21,11 +17,11 @@ Email Created - Details + Details - @if(!Model.Users.Any()) + @if(!Model.Items.Any()) { No results to list. @@ -33,7 +29,7 @@ } else { - @foreach(var user in Model.Users) + @foreach(var user in Model.Items) { @@ -48,19 +44,29 @@ @if(user.Premium) { + title="Premium, expires @(user.PremiumExpirationDate?.ToShortDateString() ?? "-")"> } else { } + @if(user.MaxStorageGb.HasValue && user.MaxStorageGb > 1) + { + + } + else + { + + } @if(user.EmailVerified) { } else { - + } @if(user.TwoFactorIsEnabled()) { diff --git a/src/Core/Repositories/IOrganizationRepository.cs b/src/Core/Repositories/IOrganizationRepository.cs index bdb36a5b6c..496283f6b9 100644 --- a/src/Core/Repositories/IOrganizationRepository.cs +++ b/src/Core/Repositories/IOrganizationRepository.cs @@ -10,6 +10,7 @@ namespace Bit.Core.Repositories { Task> GetManyByEnabledAsync(); Task> GetManyByUserIdAsync(Guid userId); + Task> SearchAsync(string name, string userEmail, bool? paid, int skip, int take); Task UpdateStorageAsync(Guid id); Task> GetManyAbilitiesAsync(); } diff --git a/src/Core/Repositories/IUserRepository.cs b/src/Core/Repositories/IUserRepository.cs index 410a2d25a8..4d0505d6d0 100644 --- a/src/Core/Repositories/IUserRepository.cs +++ b/src/Core/Repositories/IUserRepository.cs @@ -8,7 +8,7 @@ namespace Bit.Core.Repositories public interface IUserRepository : IRepository { Task GetByEmailAsync(string email); - Task> SearchByEmailAsync(string email, int skip, int take); + Task> SearchAsync(string email, int skip, int take); Task> GetManyByPremiumAsync(bool premium); Task GetPublicKeyAsync(Guid id); Task GetAccountRevisionDateAsync(Guid id); diff --git a/src/Core/Repositories/SqlServer/OrganizationRepository.cs b/src/Core/Repositories/SqlServer/OrganizationRepository.cs index fef63bd1b5..a00efdce43 100644 --- a/src/Core/Repositories/SqlServer/OrganizationRepository.cs +++ b/src/Core/Repositories/SqlServer/OrganizationRepository.cs @@ -45,6 +45,20 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task> SearchAsync(string name, string userEmail, bool? paid, + int skip, int take) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[Organization_Search]", + new { Name = name, UserEmail = userEmail, Paid = paid, Skip = skip, Take = take }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + public async Task UpdateStorageAsync(Guid id) { using(var connection = new SqlConnection(ConnectionString)) diff --git a/src/Core/Repositories/SqlServer/UserRepository.cs b/src/Core/Repositories/SqlServer/UserRepository.cs index 6444dea0d4..210a1f1038 100644 --- a/src/Core/Repositories/SqlServer/UserRepository.cs +++ b/src/Core/Repositories/SqlServer/UserRepository.cs @@ -37,12 +37,12 @@ namespace Bit.Core.Repositories.SqlServer } } - public async Task> SearchByEmailAsync(string email, int skip, int take) + public async Task> SearchAsync(string email, int skip, int take) { using(var connection = new SqlConnection(ConnectionString)) { var results = await connection.QueryAsync( - $"[{Schema}].[{Table}_SearchByEmail]", + $"[{Schema}].[{Table}_Search]", new { Email = email, Skip = skip, Take = take }, commandType: CommandType.StoredProcedure); diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index ba4db087fe..76c86c58ed 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -224,6 +224,7 @@ - + + \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Organization_Search.sql b/src/Sql/dbo/Stored Procedures/Organization_Search.sql new file mode 100644 index 0000000000..5ffa075071 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Organization_Search.sql @@ -0,0 +1,34 @@ +CREATE PROCEDURE [dbo].[Organization_Search] + @Name NVARCHAR(50), + @UserEmail NVARCHAR(50), + @Paid BIT, + @Skip INT = 0, + @Take INT = 25 +AS +BEGIN + SET NOCOUNT ON + DECLARE @NameLikeSearch NVARCHAR(55) = '%' + @Name + '%' + + SELECT + O.* + FROM + [dbo].[OrganizationView] O + INNER JOIN + [dbo].[OrganizationUser] OU ON O.[Id] = OU.[OrganizationId] + INNER JOIN + [dbo].[User] U ON U.[Id] = OU.[UserId] + WHERE + (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch) + AND (@UserEmail IS NULL OR U.[Email] = @UserEmail) + AND + ( + @Paid IS NULL OR + ( + (@Paid = 1 AND O.[GatewaySubscriptionId] IS NOT NULL) OR + (@Paid = 0 AND O.[GatewaySubscriptionId] IS NULL) + ) + ) + ORDER BY O.[CreationDate] DESC + OFFSET @Skip ROWS + FETCH NEXT @Take ROWS ONLY +END diff --git a/src/Sql/dbo/Stored Procedures/User_SearchByEmail.sql b/src/Sql/dbo/Stored Procedures/User_Search.sql similarity index 88% rename from src/Sql/dbo/Stored Procedures/User_SearchByEmail.sql rename to src/Sql/dbo/Stored Procedures/User_Search.sql index 444b0d6cf1..9e4657006c 100644 --- a/src/Sql/dbo/Stored Procedures/User_SearchByEmail.sql +++ b/src/Sql/dbo/Stored Procedures/User_Search.sql @@ -1,4 +1,4 @@ -CREATE PROCEDURE [dbo].[User_SearchByEmail] +CREATE PROCEDURE [dbo].[User_Search] @Email NVARCHAR(50), @Skip INT = 0, @Take INT = 25 diff --git a/util/Setup/DbScripts/2018-03-21_00_AdminPortal.sql b/util/Setup/DbScripts/2018-03-21_00_AdminPortal.sql index 359bed380b..3adb5958e3 100644 --- a/util/Setup/DbScripts/2018-03-21_00_AdminPortal.sql +++ b/util/Setup/DbScripts/2018-03-21_00_AdminPortal.sql @@ -1,10 +1,10 @@ -IF OBJECT_ID('[dbo].[User_SearchByEmail]') IS NOT NULL +IF OBJECT_ID('[dbo].[User_Search]') IS NOT NULL BEGIN - DROP PROCEDURE [dbo].[User_SearchByEmail] + DROP PROCEDURE [dbo].[User_Search] END GO -CREATE PROCEDURE [dbo].[User_SearchByEmail] +CREATE PROCEDURE [dbo].[User_Search] @Email NVARCHAR(50), @Skip INT = 0, @Take INT = 25 @@ -24,3 +24,45 @@ BEGIN FETCH NEXT @Take ROWS ONLY END GO + +IF OBJECT_ID('[dbo].[Organization_Search]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Organization_Search] +END +GO + +CREATE PROCEDURE [dbo].[Organization_Search] + @Name NVARCHAR(50), + @UserEmail NVARCHAR(50), + @Paid BIT, + @Skip INT = 0, + @Take INT = 25 +AS +BEGIN + SET NOCOUNT ON + DECLARE @NameLikeSearch NVARCHAR(55) = '%' + @Name + '%' + + SELECT + O.* + FROM + [dbo].[OrganizationView] O + INNER JOIN + [dbo].[OrganizationUser] OU ON O.[Id] = OU.[OrganizationId] + INNER JOIN + [dbo].[User] U ON U.[Id] = OU.[UserId] + WHERE + (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch) + AND (@UserEmail IS NULL OR U.[Email] = @UserEmail) + AND + ( + @Paid IS NULL OR + ( + (@Paid = 1 AND O.[GatewaySubscriptionId] IS NOT NULL) OR + (@Paid = 0 AND O.[GatewaySubscriptionId] IS NULL) + ) + ) + ORDER BY O.[CreationDate] DESC + OFFSET @Skip ROWS + FETCH NEXT @Take ROWS ONLY +END +GO