1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01:00

[PM-12074] - Refactored Index to use UserViewModel (#4797)

* Refactored View and Edit models to have all needed fields.
This commit is contained in:
Jared McCannon 2024-09-30 13:21:30 -05:00 committed by GitHub
parent 72b7f6c065
commit 81b151b1c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 367 additions and 153 deletions

View File

@ -1,11 +1,11 @@
using Bit.Admin.Enums; #nullable enable
using Bit.Admin.Enums;
using Bit.Admin.Models; using Bit.Admin.Models;
using Bit.Admin.Services; using Bit.Admin.Services;
using Bit.Admin.Utilities; using Bit.Admin.Utilities;
using Bit.Core; using Bit.Core;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
@ -24,9 +24,9 @@ public class UsersController : Controller
private readonly IPaymentService _paymentService; private readonly IPaymentService _paymentService;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly IAccessControlService _accessControlService; private readonly IAccessControlService _accessControlService;
private readonly ICurrentContext _currentContext;
private readonly IFeatureService _featureService;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IFeatureService _featureService;
private readonly IUserService _userService;
public UsersController( public UsersController(
IUserRepository userRepository, IUserRepository userRepository,
@ -34,18 +34,18 @@ public class UsersController : Controller
IPaymentService paymentService, IPaymentService paymentService,
GlobalSettings globalSettings, GlobalSettings globalSettings,
IAccessControlService accessControlService, IAccessControlService accessControlService,
ICurrentContext currentContext, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IFeatureService featureService, IFeatureService featureService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) IUserService userService)
{ {
_userRepository = userRepository; _userRepository = userRepository;
_cipherRepository = cipherRepository; _cipherRepository = cipherRepository;
_paymentService = paymentService; _paymentService = paymentService;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_accessControlService = accessControlService; _accessControlService = accessControlService;
_currentContext = currentContext;
_featureService = featureService;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_featureService = featureService;
_userService = userService;
} }
[RequirePermission(Permission.User_List_View)] [RequirePermission(Permission.User_List_View)]
@ -64,19 +64,26 @@ public class UsersController : Controller
var skip = (page - 1) * count; var skip = (page - 1) * count;
var users = await _userRepository.SearchAsync(email, skip, count); var users = await _userRepository.SearchAsync(email, skip, count);
var userModels = new List<UserViewModel>();
if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)) if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization))
{ {
var user2Fa = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(users.Select(u => u.Id))).ToList(); var twoFactorAuthLookup = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(users.Select(u => u.Id))).ToList();
// TempDataSerializer is having an issue serializing an empty IEnumerable<Tuple<T1,T2>>, do not set if empty.
if (user2Fa.Count != 0) 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 return View(new UsersModel
{ {
Items = users as List<User>, Items = userModels,
Email = string.IsNullOrWhiteSpace(email) ? null : email, Email = string.IsNullOrWhiteSpace(email) ? null : email,
Page = page, Page = page,
Count = count, Count = count,
@ -87,13 +94,17 @@ public class UsersController : Controller
public async Task<IActionResult> View(Guid id) public async Task<IActionResult> View(Guid id)
{ {
var user = await _userRepository.GetByIdAsync(id); var user = await _userRepository.GetByIdAsync(id);
if (user == null) if (user == null)
{ {
return RedirectToAction("Index"); return RedirectToAction("Index");
} }
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id); 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)] [SelfHosted(NotSelfHostedOnly = true)]
@ -108,7 +119,8 @@ public class UsersController : Controller
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id); var ciphers = await _cipherRepository.GetManyByUserIdAsync(id);
var billingInfo = await _paymentService.GetBillingAsync(user); var billingInfo = await _paymentService.GetBillingAsync(user);
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(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] [HttpPost]

View File

@ -7,18 +7,23 @@ using Bit.Core.Vault.Entities;
namespace Bit.Admin.Models; namespace Bit.Admin.Models;
public class UserEditModel : UserViewModel public class UserEditModel
{ {
public UserEditModel() { } public UserEditModel()
{
}
public UserEditModel( public UserEditModel(
User user, User user,
bool isTwoFactorEnabled,
IEnumerable<Cipher> ciphers, IEnumerable<Cipher> ciphers,
BillingInfo billingInfo, BillingInfo billingInfo,
BillingHistoryInfo billingHistoryInfo, BillingHistoryInfo billingHistoryInfo,
GlobalSettings globalSettings) GlobalSettings globalSettings)
: base(user, ciphers)
{ {
User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers);
BillingInfo = billingInfo; BillingInfo = billingInfo;
BillingHistoryInfo = billingHistoryInfo; BillingHistoryInfo = billingHistoryInfo;
BraintreeMerchantId = globalSettings.Braintree.MerchantId; BraintreeMerchantId = globalSettings.Braintree.MerchantId;
@ -35,32 +40,32 @@ public class UserEditModel : UserViewModel
PremiumExpirationDate = user.PremiumExpirationDate; PremiumExpirationDate = user.PremiumExpirationDate;
} }
public BillingInfo BillingInfo { get; set; } public UserViewModel User { get; init; }
public BillingHistoryInfo BillingHistoryInfo { get; set; } public BillingInfo BillingInfo { get; init; }
public BillingHistoryInfo BillingHistoryInfo { get; init; }
public string RandomLicenseKey => CoreHelpers.SecureRandomString(20); public string RandomLicenseKey => CoreHelpers.SecureRandomString(20);
public string OneYearExpirationDate => DateTime.Now.AddYears(1).ToString("yyyy-MM-ddTHH:mm"); 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")] [Display(Name = "Name")]
public string Name { get; set; } public string Name { get; init; }
[Required] [Required]
[Display(Name = "Email")] [Display(Name = "Email")]
public string Email { get; set; } public string Email { get; init; }
[Display(Name = "Email Verified")] [Display(Name = "Email Verified")]
public bool EmailVerified { get; set; } public bool EmailVerified { get; init; }
[Display(Name = "Premium")] [Display(Name = "Premium")]
public bool Premium { get; set; } public bool Premium { get; init; }
[Display(Name = "Max. Storage GB")] [Display(Name = "Max. Storage GB")]
public short? MaxStorageGb { get; set; } public short? MaxStorageGb { get; init; }
[Display(Name = "Gateway")] [Display(Name = "Gateway")]
public Core.Enums.GatewayType? Gateway { get; set; } public Core.Enums.GatewayType? Gateway { get; init; }
[Display(Name = "Gateway Customer Id")] [Display(Name = "Gateway Customer Id")]
public string GatewayCustomerId { get; set; } public string GatewayCustomerId { get; init; }
[Display(Name = "Gateway Subscription Id")] [Display(Name = "Gateway Subscription Id")]
public string GatewaySubscriptionId { get; set; } public string GatewaySubscriptionId { get; init; }
[Display(Name = "License Key")] [Display(Name = "License Key")]
public string LicenseKey { get; set; } public string LicenseKey { get; init; }
[Display(Name = "Premium Expiration Date")] [Display(Name = "Premium Expiration Date")]
public DateTime? PremiumExpirationDate { get; set; } public DateTime? PremiumExpirationDate { get; init; }
} }

View File

@ -1,18 +1,131 @@
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Vault.Entities; using Bit.Core.Vault.Entities;
namespace Bit.Admin.Models; namespace Bit.Admin.Models;
public class UserViewModel 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<Cipher> 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<Cipher> 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(); CipherCount = ciphers.Count();
} }
public User User { get; set; } public static IEnumerable<UserViewModel> MapViewModels(
public int CipherCount { get; set; } IEnumerable<User> 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<Cipher>());
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled) =>
MapViewModel(user, isTwoFactorEnabled, Array.Empty<Cipher>());
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable<Cipher> 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;
} }

View File

@ -1,8 +1,6 @@
using Bit.Core.Entities; namespace Bit.Admin.Models;
namespace Bit.Admin.Models; public class UsersModel : PagedModel<UserViewModel>
public class UsersModel : PagedModel<User>
{ {
public string Email { get; set; } public string Email { get; set; }
public string Action { get; set; } public string Action { get; set; }

View File

@ -86,7 +86,7 @@
@if (canViewUserInformation) @if (canViewUserInformation)
{ {
<h2>User Information</h2> <h2>User Information</h2>
@await Html.PartialAsync("_ViewInformation", Model) @await Html.PartialAsync("_ViewInformation", Model.User)
} }
@if (canViewBillingInformation) @if (canViewBillingInformation)
{ {

View File

@ -1,6 +1,4 @@
@model UsersModel @model UsersModel
@inject Bit.Core.Services.IUserService userService
@inject Bit.Core.Services.IFeatureService featureService
@{ @{
ViewData["Title"] = "Users"; ViewData["Title"] = "Users";
} }
@ -16,100 +14,88 @@
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-hover"> <table class="table table-striped table-hover">
<thead> <thead>
<tr> <tr>
<th>Email</th> <th>Email</th>
<th style="width: 150px;">Created</th> <th style="width: 150px;">Created</th>
<th style="width: 170px; min-width: 170px;">Details</th> <th style="width: 170px; min-width: 170px;">Details</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@if(!Model.Items.Any()) @if (!Model.Items.Any())
{
<tr>
<td colspan="4">No results to list.</td>
</tr>
}
else
{
@foreach (var user in Model.Items)
{ {
<tr> <tr>
<td colspan="4">No results to list.</td> <td>
<a asp-action="@Model.Action" asp-route-id="@user.Id">@user.Email</a>
</td>
<td>
<span title="@user.CreationDate.ToString()">
@user.CreationDate.ToShortDateString()
</span>
</td>
<td>
@if (user.Premium)
{
<i class="fa fa-star fa-lg fa-fw"
title="Premium, expires @(user.PremiumExpirationDate?.ToShortDateString() ?? "-")">
</i>
}
else
{
<i class="fa fa-star-o fa-lg fa-fw text-muted" title="Not Premium"></i>
}
@if (user.MaxStorageGb.HasValue && user.MaxStorageGb > 1)
{
<i class="fa fa-plus-square fa-lg fa-fw"
title="Additional Storage, @(user.MaxStorageGb - 1) GB">
</i>
}
else
{
<i class="fa fa-plus-square-o fa-lg fa-fw text-muted"
title="No Additional Storage">
</i>
}
@if (user.EmailVerified)
{
<i class="fa fa-check-circle fa-lg fa-fw" title="Email Verified"></i>
}
else
{
<i class="fa fa-times-circle-o fa-lg fa-fw text-muted" title="Email Not Verified"></i>
}
@if (user.TwoFactorEnabled)
{
<i class="fa fa-lock fa-lg fa-fw" title="2FA Enabled"></i>
}
else
{
<i class="fa fa-unlock fa-lg fa-fw text-muted" title="2FA Not Enabled"></i>
}
</td>
</tr> </tr>
} }
else }
{
@foreach(var user in Model.Items)
{
<tr>
<td>
<a asp-action="@Model.Action" asp-route-id="@user.Id">@user.Email</a>
</td>
<td>
<span title="@user.CreationDate.ToString()">
@user.CreationDate.ToShortDateString()
</span>
</td>
<td>
@if(user.Premium)
{
<i class="fa fa-star fa-lg fa-fw"
title="Premium, expires @(user.PremiumExpirationDate?.ToShortDateString() ?? "-")"></i>
}
else
{
<i class="fa fa-star-o fa-lg fa-fw text-muted" title="Not Premium"></i>
}
@if(user.MaxStorageGb.HasValue && user.MaxStorageGb > 1)
{
<i class="fa fa-plus-square fa-lg fa-fw"
title="Additional Storage, @(user.MaxStorageGb - 1) GB"></i>
}
else
{
<i class="fa fa-plus-square-o fa-lg fa-fw text-muted"
title="No Additional Storage"></i>
}
@if(user.EmailVerified)
{
<i class="fa fa-check-circle fa-lg fa-fw" title="Email Verified"></i>
}
else
{
<i class="fa fa-times-circle-o fa-lg fa-fw text-muted" title="Email Not Verified"></i>
}
@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 })
{
<i class="fa fa-lock fa-lg fa-fw" title="2FA Enabled"></i>
}
else
{
<i class="fa fa-unlock fa-lg fa-fw text-muted" title="2FA Not Enabled"></i>
}
}
else
{
@if(await userService.TwoFactorIsEnabledAsync(user))
{
<i class="fa fa-lock fa-lg fa-fw" title="2FA Enabled"></i>
}
else
{
<i class="fa fa-unlock fa-lg fa-fw text-muted" title="2FA Not Enabled"></i>
}
}
</td>
</tr>
}
}
</tbody> </tbody>
</table> </table>
</div> </div>
<nav> <nav>
<ul class="pagination"> <ul class="pagination">
@if(Model.PreviousPage.HasValue) @if (Model.PreviousPage.HasValue)
{ {
<li class="page-item"> <li class="page-item">
<a class="page-link" asp-action="Index" asp-route-page="@Model.PreviousPage.Value" <a class="page-link" asp-action="Index" asp-route-page="@Model.PreviousPage.Value"
asp-route-count="@Model.Count" asp-route-email="@Model.Email">Previous</a> asp-route-count="@Model.Count" asp-route-email="@Model.Email">
Previous
</a>
</li> </li>
} }
else else
@ -118,11 +104,13 @@
<a class="page-link" href="#" tabindex="-1">Previous</a> <a class="page-link" href="#" tabindex="-1">Previous</a>
</li> </li>
} }
@if(Model.NextPage.HasValue) @if (Model.NextPage.HasValue)
{ {
<li class="page-item"> <li class="page-item">
<a class="page-link" asp-action="Index" asp-route-page="@Model.NextPage.Value" <a class="page-link" asp-action="Index" asp-route-page="@Model.NextPage.Value"
asp-route-count="@Model.Count" asp-route-email="@Model.Email">Next</a> asp-route-count="@Model.Count" asp-route-email="@Model.Email">
Next
</a>
</li> </li>
} }
else else

View File

@ -1,13 +1,13 @@
@model UserViewModel @model UserViewModel
@{ @{
ViewData["Title"] = "User: " + Model.User.Email; ViewData["Title"] = "User: " + Model.Email;
} }
<h1>User <small>@Model.User.Email</small></h1> <h1>User <small>@Model.Email</small></h1>
<h2>Information</h2> <h2>Information</h2>
@await Html.PartialAsync("_ViewInformation", Model) @await Html.PartialAsync("_ViewInformation", Model)
<form asp-action="Delete" asp-route-id="@Model.User.Id" <form asp-action="Delete" asp-route-id="@Model.Id"
onsubmit="return confirm('Are you sure you want to delete this user?')"> onsubmit="return confirm('Are you sure you want to delete this user?')">
<button class="btn btn-danger" type="submit">Delete</button> <button class="btn btn-danger" type="submit">Delete</button>
</form> </form>

View File

@ -1,43 +1,42 @@
@model UserViewModel @model UserViewModel
@inject Bit.Core.Services.IUserService userService
<dl class="row"> <dl class="row">
<dt class="col-sm-4 col-lg-3">Id</dt> <dt class="col-sm-4 col-lg-3">Id</dt>
<dd class="col-sm-8 col-lg-9"><code>@Model.User.Id</code></dd> <dd class="col-sm-8 col-lg-9"><code>@Model.Id</code></dd>
<dt class="col-sm-4 col-lg-3">Premium</dt> <dt class="col-sm-4 col-lg-3">Premium</dt>
<dd class="col-sm-8 col-lg-9">@(Model.User.Premium ? "Yes" : "No")</dd> <dd class="col-sm-8 col-lg-9">@(Model.Premium ? "Yes" : "No")</dd>
<dt class="col-sm-4 col-lg-3">Premium Expires</dt> <dt class="col-sm-4 col-lg-3">Premium Expires</dt>
<dd class="col-sm-8 col-lg-9">@(Model.User.PremiumExpirationDate?.ToString() ?? "-")</dd> <dd class="col-sm-8 col-lg-9">@(Model.PremiumExpirationDate?.ToString() ?? "-")</dd>
<dt class="col-sm-4 col-lg-3">Email Verified</dt> <dt class="col-sm-4 col-lg-3">Email Verified</dt>
<dd class="col-sm-8 col-lg-9">@(Model.User.EmailVerified ? "Yes" : "No")</dd> <dd class="col-sm-8 col-lg-9">@(Model.EmailVerified ? "Yes" : "No")</dd>
<dt class="col-sm-4 col-lg-3">Using 2FA</dt> <dt class="col-sm-4 col-lg-3">Using 2FA</dt>
<dd class="col-sm-8 col-lg-9">@(await userService.TwoFactorIsEnabledAsync(Model.User) ? "Yes" : "No")</dd> <dd class="col-sm-8 col-lg-9">@(Model.TwoFactorEnabled ? "Yes" : "No")</dd>
<dt class="col-sm-4 col-lg-3">Items</dt> <dt class="col-sm-4 col-lg-3">Items</dt>
<dd class="col-sm-8 col-lg-9">@Model.CipherCount</dd> <dd class="col-sm-8 col-lg-9">@Model.CipherCount</dd>
<dt class="col-sm-4 col-lg-3">Vault Modified</dt> <dt class="col-sm-4 col-lg-3">Vault Modified</dt>
<dd class="col-sm-8 col-lg-9">@Model.User.AccountRevisionDate.ToString()</dd> <dd class="col-sm-8 col-lg-9">@Model.AccountRevisionDate.ToString()</dd>
<dt class="col-sm-4 col-lg-3">Created</dt> <dt class="col-sm-4 col-lg-3">Created</dt>
<dd class="col-sm-8 col-lg-9">@Model.User.CreationDate.ToString()</dd> <dd class="col-sm-8 col-lg-9">@Model.CreationDate.ToString()</dd>
<dt class="col-sm-4 col-lg-3">Modified</dt> <dt class="col-sm-4 col-lg-3">Modified</dt>
<dd class="col-sm-8 col-lg-9">@Model.User.RevisionDate.ToString()</dd> <dd class="col-sm-8 col-lg-9">@Model.RevisionDate.ToString()</dd>
<dt class="col-sm-4 col-lg-3">Last Email Address Change</dt> <dt class="col-sm-4 col-lg-3">Last Email Address Change</dt>
<dd class="col-sm-8 col-lg-9">@(Model.User.LastEmailChangeDate?.ToString() ?? "-")</dd> <dd class="col-sm-8 col-lg-9">@(Model.LastEmailChangeDate?.ToString() ?? "-")</dd>
<dt class="col-sm-4 col-lg-3">Last KDF Change</dt> <dt class="col-sm-4 col-lg-3">Last KDF Change</dt>
<dd class="col-sm-8 col-lg-9">@(Model.User.LastKdfChangeDate?.ToString() ?? "-")</dd> <dd class="col-sm-8 col-lg-9">@(Model.LastKdfChangeDate?.ToString() ?? "-")</dd>
<dt class="col-sm-4 col-lg-3">Last Key Rotation</dt> <dt class="col-sm-4 col-lg-3">Last Key Rotation</dt>
<dd class="col-sm-8 col-lg-9">@(Model.User.LastKeyRotationDate?.ToString() ?? "-")</dd> <dd class="col-sm-8 col-lg-9">@(Model.LastKeyRotationDate?.ToString() ?? "-")</dd>
<dt class="col-sm-4 col-lg-3">Last Password Change</dt> <dt class="col-sm-4 col-lg-3">Last Password Change</dt>
<dd class="col-sm-8 col-lg-9">@(Model.User.LastPasswordChangeDate?.ToString() ?? "-")</dd> <dd class="col-sm-8 col-lg-9">@(Model.LastPasswordChangeDate?.ToString() ?? "-")</dd>
</dl> </dl>

View File

@ -18,5 +18,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Admin\Admin.csproj" /> <ProjectReference Include="..\..\src\Admin\Admin.csproj" />
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,108 @@
#nullable enable
using Bit.Admin.Models;
using Bit.Core.Entities;
using Bit.Test.Common.AutoFixture.Attributes;
namespace Admin.Test.Models;
public class UserViewModelTests
{
[Theory]
[BitAutoData]
public void IsTwoFactorEnabled_GivenUserAndIsInLookup_WhenUserHasTwoFactorEnabled_ThenReturnsTrue(User user)
{
var lookup = new List<(Guid, bool)>
{
(user.Id, true)
};
var actual = UserViewModel.IsTwoFactorEnabled(user, lookup);
Assert.True(actual);
}
[Theory]
[BitAutoData]
public void IsTwoFactorEnabled_GivenUserAndIsInLookup_WhenUserDoesNotHaveTwoFactorEnabled_ThenReturnsFalse(User user)
{
var lookup = new List<(Guid, bool)>
{
(Guid.NewGuid(), true)
};
var actual = UserViewModel.IsTwoFactorEnabled(user, lookup);
Assert.False(actual);
}
[Theory]
[BitAutoData]
public void IsTwoFactorEnabled_GivenUserAndIsNotInLookup_WhenUserDoesNotHaveTwoFactorEnabled_ThenReturnsFalse(User user)
{
var lookup = new List<(Guid, bool)>();
var actual = UserViewModel.IsTwoFactorEnabled(user, lookup);
Assert.False(actual);
}
[Theory]
[BitAutoData]
public void MapUserViewModel_GivenUser_WhenPopulated_ThenMapsToUserViewModel(User user)
{
var actual = UserViewModel.MapViewModel(user, true);
Assert.Equal(actual.Id, user.Id);
Assert.Equal(actual.Email, user.Email);
Assert.Equal(actual.CreationDate, user.CreationDate);
Assert.Equal(actual.PremiumExpirationDate, user.PremiumExpirationDate);
Assert.Equal(actual.Premium, user.Premium);
Assert.Equal(actual.MaxStorageGb, user.MaxStorageGb);
Assert.Equal(actual.EmailVerified, user.EmailVerified);
Assert.True(actual.TwoFactorEnabled);
Assert.Equal(actual.AccountRevisionDate, user.AccountRevisionDate);
Assert.Equal(actual.RevisionDate, user.RevisionDate);
Assert.Equal(actual.LastEmailChangeDate, user.LastEmailChangeDate);
Assert.Equal(actual.LastKdfChangeDate, user.LastKdfChangeDate);
Assert.Equal(actual.LastKeyRotationDate, user.LastKeyRotationDate);
Assert.Equal(actual.LastPasswordChangeDate, user.LastPasswordChangeDate);
Assert.Equal(actual.Gateway, user.Gateway);
Assert.Equal(actual.GatewayCustomerId, user.GatewayCustomerId);
Assert.Equal(actual.GatewaySubscriptionId, user.GatewaySubscriptionId);
Assert.Equal(actual.LicenseKey, user.LicenseKey);
}
[Theory]
[BitAutoData]
public void MapUserViewModel_GivenUserWithTwoFactorEnabled_WhenPopulated_ThenMapsToUserViewModel(User user)
{
var lookup = new List<(Guid, bool)> { (user.Id, true) };
var actual = UserViewModel.MapViewModel(user, lookup);
Assert.True(actual.TwoFactorEnabled);
}
[Theory]
[BitAutoData]
public void MapUserViewModel_GivenUserWithoutTwoFactorEnabled_WhenPopulated_ThenTwoFactorIsEnabled(User user)
{
var lookup = new List<(Guid, bool)> { (user.Id, false) };
var actual = UserViewModel.MapViewModel(user, lookup);
Assert.False(actual.TwoFactorEnabled);
}
[Theory]
[BitAutoData]
public void MapUserViewModel_GivenUser_WhenNotInLookUpList_ThenTwoFactorIsDisabled(User user)
{
var lookup = new List<(Guid, bool)> { (Guid.NewGuid(), true) };
var actual = UserViewModel.MapViewModel(user, lookup);
Assert.False(actual.TwoFactorEnabled);
}
}

View File

@ -1,10 +0,0 @@
namespace Admin.Test;
// Delete this file once you have real tests
public class PlaceholderUnitTest
{
[Fact]
public void Test1()
{
}
}

View File

@ -35,7 +35,7 @@ public class OrganizationUsersControllerTests
{ {
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task PutResetPasswordEnrollment_InivitedUser_AcceptsInvite(Guid orgId, Guid userId, OrganizationUserResetPasswordEnrollmentRequestModel model, public async Task PutResetPasswordEnrollment_InvitedUser_AcceptsInvite(Guid orgId, Guid userId, OrganizationUserResetPasswordEnrollmentRequestModel model,
User user, OrganizationUser orgUser, SutProvider<OrganizationUsersController> sutProvider) User user, OrganizationUser orgUser, SutProvider<OrganizationUsersController> sutProvider)
{ {
orgUser.Status = Core.Enums.OrganizationUserStatusType.Invited; orgUser.Status = Core.Enums.OrganizationUserStatusType.Invited;