diff --git a/src/Admin/Controllers/UsersController.cs b/src/Admin/Controllers/UsersController.cs index e233b61e4..842abaea6 100644 --- a/src/Admin/Controllers/UsersController.cs +++ b/src/Admin/Controllers/UsersController.cs @@ -1,11 +1,11 @@ -using Bit.Admin.Enums; +#nullable enable + +using Bit.Admin.Enums; using Bit.Admin.Models; using Bit.Admin.Services; using Bit.Admin.Utilities; using Bit.Core; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; -using Bit.Core.Context; -using Bit.Core.Entities; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -24,9 +24,9 @@ public class UsersController : Controller private readonly IPaymentService _paymentService; private readonly GlobalSettings _globalSettings; private readonly IAccessControlService _accessControlService; - private readonly ICurrentContext _currentContext; - private readonly IFeatureService _featureService; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; + private readonly IFeatureService _featureService; + private readonly IUserService _userService; public UsersController( IUserRepository userRepository, @@ -34,18 +34,18 @@ public class UsersController : Controller IPaymentService paymentService, GlobalSettings globalSettings, IAccessControlService accessControlService, - ICurrentContext currentContext, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IFeatureService featureService, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) + IUserService userService) { _userRepository = userRepository; _cipherRepository = cipherRepository; _paymentService = paymentService; _globalSettings = globalSettings; _accessControlService = accessControlService; - _currentContext = currentContext; - _featureService = featureService; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; + _featureService = featureService; + _userService = userService; } [RequirePermission(Permission.User_List_View)] @@ -64,19 +64,26 @@ public class UsersController : Controller var skip = (page - 1) * count; var users = await _userRepository.SearchAsync(email, skip, count); + var userModels = new List(); + if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)) { - var user2Fa = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(users.Select(u => u.Id))).ToList(); - // TempDataSerializer is having an issue serializing an empty IEnumerable>, do not set if empty. - if (user2Fa.Count != 0) + var twoFactorAuthLookup = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(users.Select(u => u.Id))).ToList(); + + userModels = UserViewModel.MapViewModels(users, twoFactorAuthLookup).ToList(); + } + else + { + foreach (var user in users) { - TempData["UsersTwoFactorIsEnabled"] = user2Fa; + var isTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); + userModels.Add(UserViewModel.MapViewModel(user, isTwoFactorEnabled)); } } return View(new UsersModel { - Items = users as List, + Items = userModels, Email = string.IsNullOrWhiteSpace(email) ? null : email, Page = page, Count = count, @@ -87,13 +94,17 @@ public class UsersController : Controller public async Task View(Guid id) { var user = await _userRepository.GetByIdAsync(id); + if (user == null) { return RedirectToAction("Index"); } var ciphers = await _cipherRepository.GetManyByUserIdAsync(id); - return View(new UserViewModel(user, ciphers)); + + var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); + + return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers)); } [SelfHosted(NotSelfHostedOnly = true)] @@ -108,7 +119,8 @@ public class UsersController : Controller var ciphers = await _cipherRepository.GetManyByUserIdAsync(id); var billingInfo = await _paymentService.GetBillingAsync(user); var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user); - return View(new UserEditModel(user, ciphers, billingInfo, billingHistoryInfo, _globalSettings)); + var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); + return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings)); } [HttpPost] diff --git a/src/Admin/Models/UserEditModel.cs b/src/Admin/Models/UserEditModel.cs index f739af199..52cdb4c80 100644 --- a/src/Admin/Models/UserEditModel.cs +++ b/src/Admin/Models/UserEditModel.cs @@ -7,18 +7,23 @@ using Bit.Core.Vault.Entities; namespace Bit.Admin.Models; -public class UserEditModel : UserViewModel +public class UserEditModel { - public UserEditModel() { } + public UserEditModel() + { + + } public UserEditModel( User user, + bool isTwoFactorEnabled, IEnumerable ciphers, BillingInfo billingInfo, BillingHistoryInfo billingHistoryInfo, GlobalSettings globalSettings) - : base(user, ciphers) { + User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers); + BillingInfo = billingInfo; BillingHistoryInfo = billingHistoryInfo; BraintreeMerchantId = globalSettings.Braintree.MerchantId; @@ -35,32 +40,32 @@ public class UserEditModel : UserViewModel PremiumExpirationDate = user.PremiumExpirationDate; } - public BillingInfo BillingInfo { get; set; } - public BillingHistoryInfo BillingHistoryInfo { get; set; } + public UserViewModel User { get; init; } + public BillingInfo BillingInfo { get; init; } + public BillingHistoryInfo BillingHistoryInfo { get; init; } public string RandomLicenseKey => CoreHelpers.SecureRandomString(20); public string OneYearExpirationDate => DateTime.Now.AddYears(1).ToString("yyyy-MM-ddTHH:mm"); - public string BraintreeMerchantId { get; set; } + public string BraintreeMerchantId { get; init; } [Display(Name = "Name")] - public string Name { get; set; } + public string Name { get; init; } [Required] [Display(Name = "Email")] - public string Email { get; set; } + public string Email { get; init; } [Display(Name = "Email Verified")] - public bool EmailVerified { get; set; } + public bool EmailVerified { get; init; } [Display(Name = "Premium")] - public bool Premium { get; set; } + public bool Premium { get; init; } [Display(Name = "Max. Storage GB")] - public short? MaxStorageGb { get; set; } + public short? MaxStorageGb { get; init; } [Display(Name = "Gateway")] - public Core.Enums.GatewayType? Gateway { get; set; } + public Core.Enums.GatewayType? Gateway { get; init; } [Display(Name = "Gateway Customer Id")] - public string GatewayCustomerId { get; set; } + public string GatewayCustomerId { get; init; } [Display(Name = "Gateway Subscription Id")] - public string GatewaySubscriptionId { get; set; } + public string GatewaySubscriptionId { get; init; } [Display(Name = "License Key")] - public string LicenseKey { get; set; } + public string LicenseKey { get; init; } [Display(Name = "Premium Expiration Date")] - public DateTime? PremiumExpirationDate { get; set; } - + public DateTime? PremiumExpirationDate { get; init; } } diff --git a/src/Admin/Models/UserViewModel.cs b/src/Admin/Models/UserViewModel.cs index 05160f2e0..09b3d5577 100644 --- a/src/Admin/Models/UserViewModel.cs +++ b/src/Admin/Models/UserViewModel.cs @@ -1,18 +1,131 @@ using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Vault.Entities; namespace Bit.Admin.Models; public class UserViewModel { - public UserViewModel() { } + public Guid Id { get; } + public string Name { get; } + public string Email { get; } + public DateTime CreationDate { get; } + public DateTime? PremiumExpirationDate { get; } + public bool Premium { get; } + public short? MaxStorageGb { get; } + public bool EmailVerified { get; } + public bool TwoFactorEnabled { get; } + public DateTime AccountRevisionDate { get; } + public DateTime RevisionDate { get; } + public DateTime? LastEmailChangeDate { get; } + public DateTime? LastKdfChangeDate { get; } + public DateTime? LastKeyRotationDate { get; } + public DateTime? LastPasswordChangeDate { get; } + public GatewayType? Gateway { get; } + public string GatewayCustomerId { get; } + public string GatewaySubscriptionId { get; } + public string LicenseKey { get; } + public int CipherCount { get; set; } - public UserViewModel(User user, IEnumerable ciphers) + public UserViewModel(Guid id, + string name, + string email, + DateTime creationDate, + DateTime? premiumExpirationDate, + bool premium, + short? maxStorageGb, + bool emailVerified, + bool twoFactorEnabled, + DateTime accountRevisionDate, + DateTime revisionDate, + DateTime? lastEmailChangeDate, + DateTime? lastKdfChangeDate, + DateTime? lastKeyRotationDate, + DateTime? lastPasswordChangeDate, + GatewayType? gateway, + string gatewayCustomerId, + string gatewaySubscriptionId, + string licenseKey, + IEnumerable ciphers) { - User = user; + Id = id; + Name = name; + Email = email; + CreationDate = creationDate; + PremiumExpirationDate = premiumExpirationDate; + Premium = premium; + MaxStorageGb = maxStorageGb; + EmailVerified = emailVerified; + TwoFactorEnabled = twoFactorEnabled; + AccountRevisionDate = accountRevisionDate; + RevisionDate = revisionDate; + LastEmailChangeDate = lastEmailChangeDate; + LastKdfChangeDate = lastKdfChangeDate; + LastKeyRotationDate = lastKeyRotationDate; + LastPasswordChangeDate = lastPasswordChangeDate; + Gateway = gateway; + GatewayCustomerId = gatewayCustomerId; + GatewaySubscriptionId = gatewaySubscriptionId; + LicenseKey = licenseKey; CipherCount = ciphers.Count(); } - public User User { get; set; } - public int CipherCount { get; set; } + public static IEnumerable MapViewModels( + IEnumerable users, + IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup) => + users.Select(user => MapViewModel(user, lookup)); + + public static UserViewModel MapViewModel(User user, + IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup) => + new( + user.Id, + user.Name, + user.Email, + user.CreationDate, + user.PremiumExpirationDate, + user.Premium, + user.MaxStorageGb, + user.EmailVerified, + IsTwoFactorEnabled(user, lookup), + user.AccountRevisionDate, + user.RevisionDate, + user.LastEmailChangeDate, + user.LastKdfChangeDate, + user.LastKeyRotationDate, + user.LastPasswordChangeDate, + user.Gateway, + user.GatewayCustomerId ?? string.Empty, + user.GatewaySubscriptionId ?? string.Empty, + user.LicenseKey ?? string.Empty, + Array.Empty()); + + public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled) => + MapViewModel(user, isTwoFactorEnabled, Array.Empty()); + + public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable ciphers) => + new( + user.Id, + user.Name, + user.Email, + user.CreationDate, + user.PremiumExpirationDate, + user.Premium, + user.MaxStorageGb, + user.EmailVerified, + isTwoFactorEnabled, + user.AccountRevisionDate, + user.RevisionDate, + user.LastEmailChangeDate, + user.LastKdfChangeDate, + user.LastKeyRotationDate, + user.LastPasswordChangeDate, + user.Gateway, + user.GatewayCustomerId ?? string.Empty, + user.GatewaySubscriptionId ?? string.Empty, + user.LicenseKey ?? string.Empty, + ciphers); + + public static bool IsTwoFactorEnabled(User user, + IEnumerable<(Guid userId, bool twoFactorIsEnabled)> twoFactorIsEnabledLookup) => + twoFactorIsEnabledLookup.FirstOrDefault(x => x.userId == user.Id).twoFactorIsEnabled; } diff --git a/src/Admin/Models/UsersModel.cs b/src/Admin/Models/UsersModel.cs index 0a54e318d..33148301b 100644 --- a/src/Admin/Models/UsersModel.cs +++ b/src/Admin/Models/UsersModel.cs @@ -1,8 +1,6 @@ -using Bit.Core.Entities; +namespace Bit.Admin.Models; -namespace Bit.Admin.Models; - -public class UsersModel : PagedModel +public class UsersModel : PagedModel { public string Email { get; set; } public string Action { get; set; } diff --git a/src/Admin/Views/Users/Edit.cshtml b/src/Admin/Views/Users/Edit.cshtml index 2bc326d22..8f07b12a7 100644 --- a/src/Admin/Views/Users/Edit.cshtml +++ b/src/Admin/Views/Users/Edit.cshtml @@ -86,7 +86,7 @@ @if (canViewUserInformation) {

User Information

- @await Html.PartialAsync("_ViewInformation", Model) + @await Html.PartialAsync("_ViewInformation", Model.User) } @if (canViewBillingInformation) { diff --git a/src/Admin/Views/Users/Index.cshtml b/src/Admin/Views/Users/Index.cshtml index 46419503f..a53580350 100644 --- a/src/Admin/Views/Users/Index.cshtml +++ b/src/Admin/Views/Users/Index.cshtml @@ -1,6 +1,4 @@ @model UsersModel -@inject Bit.Core.Services.IUserService userService -@inject Bit.Core.Services.IFeatureService featureService @{ ViewData["Title"] = "Users"; } @@ -16,100 +14,88 @@
- - - - - + + + + + - @if(!Model.Items.Any()) + @if (!Model.Items.Any()) + { + + + + } + else + { + @foreach (var user in Model.Items) { - + + + } - else - { - @foreach(var user in Model.Items) - { - - - - - - } - } + }
EmailCreatedDetails
EmailCreatedDetails
No results to list.
No results to list. + @user.Email + + + @user.CreationDate.ToShortDateString() + + + @if (user.Premium) + { + + + } + else + { + + } + @if (user.MaxStorageGb.HasValue && user.MaxStorageGb > 1) + { + + + } + else + { + + + } + @if (user.EmailVerified) + { + + } + else + { + + } + @if (user.TwoFactorEnabled) + { + + } + else + { + + } +
- @user.Email - - - @user.CreationDate.ToShortDateString() - - - @if(user.Premium) - { - - } - else - { - - } - @if(user.MaxStorageGb.HasValue && user.MaxStorageGb > 1) - { - - } - else - { - - } - @if(user.EmailVerified) - { - - } - else - { - - } - @if (featureService.IsEnabled(Bit.Core.FeatureFlagKeys.MembersTwoFAQueryOptimization)) - { - var usersTwoFactorIsEnabled = TempData["UsersTwoFactorIsEnabled"] as IEnumerable<(Guid userId, bool twoFactorIsEnabled)>; - var matchingUser2Fa = usersTwoFactorIsEnabled?.FirstOrDefault(tuple => tuple.userId == user.Id); - - @if(matchingUser2Fa is { twoFactorIsEnabled: true }) - { - - } - else - { - - } - } - else - { - @if(await userService.TwoFactorIsEnabledAsync(user)) - { - - } - else - { - - } - } -