Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/GovUK.Dfe.FlexForms.Api.Client/Generated/Client.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10788,7 +10788,7 @@ public string BaseUrl
/// </summary>
/// <returns>Tenant users.</returns>
/// <exception cref="ExternalApplicationsException">A server side error occurred.</exception>
public virtual async System.Threading.Tasks.Task<PagedResultOfTenantUserDto> GetTenantUsersAsync(int? pageNumber = null, int? pageSize = null, System.Guid? userId = null, string email = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
public virtual async System.Threading.Tasks.Task<PagedResultOfTenantUserDto> GetTenantUsersAsync(int? pageNumber = null, int? pageSize = null, System.Guid? userId = null, string email = null, string searchTerm = null, string role = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
{
var client_ = _httpClient;
var disposeClient_ = false;
Expand Down Expand Up @@ -10820,6 +10820,14 @@ public string BaseUrl
{
urlBuilder_.Append(System.Uri.EscapeDataString("email")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(email, System.Globalization.CultureInfo.InvariantCulture))).Append('&');
}
if (searchTerm != null)
{
urlBuilder_.Append(System.Uri.EscapeDataString("searchTerm")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(searchTerm, System.Globalization.CultureInfo.InvariantCulture))).Append('&');
}
if (role != null)
{
urlBuilder_.Append(System.Uri.EscapeDataString("role")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(role, System.Globalization.CultureInfo.InvariantCulture))).Append('&');
}
urlBuilder_.Length--;

PrepareRequest(client_, request_, urlBuilder_);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ public partial interface IUsersClient
/// </summary>
/// <returns>Tenant users.</returns>
/// <exception cref="ExternalApplicationsException">A server side error occurred.</exception>
System.Threading.Tasks.Task<PagedResultOfTenantUserDto> GetTenantUsersAsync(int? pageNumber = null, int? pageSize = null, System.Guid? userId = null, string email = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
System.Threading.Tasks.Task<PagedResultOfTenantUserDto> GetTenantUsersAsync(int? pageNumber = null, int? pageSize = null, System.Guid? userId = null, string email = null, string searchTerm = null, string role = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));

/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <summary>
Expand Down
18 changes: 18 additions & 0 deletions src/GovUK.Dfe.FlexForms.Api.Client/Generated/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -5462,6 +5462,24 @@
"nullable": true
},
"x-position": 4
},
{
"name": "searchTerm",
"in": "query",
"schema": {
"type": "string",
"nullable": true
},
"x-position": 5
},
{
"name": "role",
"in": "query",
"schema": {
"type": "string",
"nullable": true
},
"x-position": 6
}
],
"responses": {
Expand Down
6 changes: 5 additions & 1 deletion src/GovUK.Dfe.FlexForms.Api/Controllers/UsersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,18 @@ public async Task<ActionResult<PagedResult<TenantUserDto>>> GetTenantUsersAsync(
[FromQuery] int? pageSize,
[FromQuery] Guid? userId,
[FromQuery] string? email,
[FromQuery] string? searchTerm,
[FromQuery] string? role,
CancellationToken cancellationToken)
{
var result = await sender.Send(
new GetTenantUsersQuery(
pageNumber ?? 1,
pageSize ?? GetTenantUsersQuery.DefaultPageSize,
userId,
email),
email,
searchTerm,
role),
cancellationToken);

if (!result.IsSuccess)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ namespace GovUK.Dfe.FlexForms.Application.TenantMemberships.QueryObjects;
public sealed class GetActiveTenantMembershipsForDirectoryQueryObject(
Guid tenantId,
UserId? userId = null,
string? email = null)
string? email = null,
string? searchTerm = null,
string? role = null)
: IQueryObject<TenantMembership>
{
public IQueryable<TenantMembership> Apply(IQueryable<TenantMembership> query)
Expand All @@ -32,6 +34,12 @@ public IQueryable<TenantMembership> Apply(IQueryable<TenantMembership> query)
query = query.Where(m => m.User != null && m.User.Email.ToLower() == normalized);
}

if (!string.IsNullOrWhiteSpace(searchTerm))
query = new GetTenantMembershipsBySearchTermQueryObject(searchTerm).Apply(query);

if (!string.IsNullOrWhiteSpace(role))
query = new GetTenantMembershipsByRoleNameQueryObject(role).Apply(query);

return query
.OrderBy(m => m.User!.Name)
.ThenBy(m => m.User!.Email);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using GovUK.Dfe.FlexForms.Application.Common.QueriesObjects;
using GovUK.Dfe.FlexForms.Domain.Entities;

namespace GovUK.Dfe.FlexForms.Application.TenantMemberships.QueryObjects;

/// <summary>
/// Restricts memberships to a single role, matched on the role name assigned within the tenant.
/// </summary>
public sealed class GetTenantMembershipsByRoleNameQueryObject(string roleName)
: IQueryObject<TenantMembership>
{
private readonly string _roleName = roleName.Trim().ToLowerInvariant();

public IQueryable<TenantMembership> Apply(IQueryable<TenantMembership> query) =>
query.Where(m => m.Role != null && m.Role.Name.ToLower() == _roleName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using GovUK.Dfe.FlexForms.Application.Common.QueriesObjects;
using GovUK.Dfe.FlexForms.Domain.Entities;

namespace GovUK.Dfe.FlexForms.Application.TenantMemberships.QueryObjects;

/// <summary>
/// Free-text filter matching part of the member's name or email address.
/// </summary>
public sealed class GetTenantMembershipsBySearchTermQueryObject(string searchTerm)
: IQueryObject<TenantMembership>
{
private readonly string _searchTerm = searchTerm.Trim().ToLowerInvariant();

public IQueryable<TenantMembership> Apply(IQueryable<TenantMembership> query) =>
query.Where(m =>
m.User != null
&& (m.User.Name.ToLower().Contains(_searchTerm)
|| m.User.Email.ToLower().Contains(_searchTerm)));
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,16 @@ public sealed record GetTenantUsersQuery(
int PageNumber = 1,
int PageSize = 10,
Guid? UserId = null,
string? Email = null)
string? Email = null,
string? SearchTerm = null,
string? Role = null)
: IRequest<Result<PagedResult<TenantUserDto>>>
{
public const int DefaultPageSize = 10;

public const int MaxPageSize = 100;

public const int MaxSearchTermLength = 256;
}

/// <summary>
Expand Down Expand Up @@ -60,7 +64,9 @@ public async Task<Result<PagedResult<TenantUserDto>>> Handle(
var membershipQuery = new GetActiveTenantMembershipsForDirectoryQueryObject(
currentTenant.Id,
request.UserId is null ? null : new UserId(request.UserId.Value),
request.Email)
request.Email,
request.SearchTerm,
request.Role)
.Apply(membershipRepository.Query());

var totalCount = await membershipQuery.CountAsync(cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,13 @@ public GetTenantUsersQueryValidator()
RuleFor(x => x.Email!)
.EmailAddress();
});

RuleFor(x => x.SearchTerm!)
.MaximumLength(GetTenantUsersQuery.MaxSearchTermLength)
.When(x => !string.IsNullOrWhiteSpace(x.SearchTerm));

RuleFor(x => x.Role!)
.MaximumLength(50)
.When(x => !string.IsNullOrWhiteSpace(x.Role));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ private static void ConfigureTenantMembership(EntityTypeBuilder<TenantMembership
.HasDatabaseName("IX_TenantMemberships_TenantId_UserId");
b.HasIndex(e => e.UserId)
.HasDatabaseName("IX_TenantMemberships_UserId");
// Serves the User Manager directory listing, which always scopes to an active tenant
// membership and optionally narrows to a single role.
b.HasIndex(e => new { e.TenantId, e.IsActive, e.RoleId })
.HasDatabaseName("IX_TenantMemberships_TenantId_IsActive_RoleId");

if (useTemporal)
{
Expand Down Expand Up @@ -383,6 +387,9 @@ private static void ConfigureUser(EntityTypeBuilder<User> b, bool useTemporal)
.IsUnicode(false);
b.HasIndex(u => u.ExternalProviderId).IsUnique();
b.HasIndex(e => e.Email).IsUnique();
// The tenant user directory orders by name, so keep a sorted copy available.
b.HasIndex(e => e.Name)
.HasDatabaseName("IX_Users_Name");
b.HasOne(e => e.Role)
.WithMany()
.HasForeignKey(e => e.RoleId);
Expand Down
Loading
Loading