feat(07-03): implement GraphUserSearchService for people-picker autocomplete
- Queries Graph /users with startsWith filter on displayName, mail, UPN - Requires minimum 2 chars to prevent overly broad queries - Sets ConsistencyLevel=eventual + Count=true (required for advanced filter) - Escapes single quotes to prevent OData injection - Returns up to maxResults (default 10) GraphUserResult records
This commit is contained in:
53
SharepointToolbox/Services/GraphUserSearchService.cs
Normal file
53
SharepointToolbox/Services/GraphUserSearchService.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using SharepointToolbox.Infrastructure.Auth;
|
||||||
|
|
||||||
|
namespace SharepointToolbox.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Searches tenant users via Microsoft Graph API.
|
||||||
|
/// Used by the people-picker autocomplete in the User Access Audit tab.
|
||||||
|
/// </summary>
|
||||||
|
public class GraphUserSearchService : IGraphUserSearchService
|
||||||
|
{
|
||||||
|
private readonly GraphClientFactory _graphClientFactory;
|
||||||
|
|
||||||
|
public GraphUserSearchService(GraphClientFactory graphClientFactory)
|
||||||
|
{
|
||||||
|
_graphClientFactory = graphClientFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<GraphUserResult>> SearchUsersAsync(
|
||||||
|
string clientId,
|
||||||
|
string query,
|
||||||
|
int maxResults = 10,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(query) || query.Length < 2)
|
||||||
|
return Array.Empty<GraphUserResult>();
|
||||||
|
|
||||||
|
var graphClient = await _graphClientFactory.CreateClientAsync(clientId, ct);
|
||||||
|
|
||||||
|
// Use $filter with startsWith on displayName and mail.
|
||||||
|
// Graph API requires ConsistencyLevel=eventual for advanced queries.
|
||||||
|
var escapedQuery = query.Replace("'", "''");
|
||||||
|
var response = await graphClient.Users.GetAsync(config =>
|
||||||
|
{
|
||||||
|
config.QueryParameters.Filter =
|
||||||
|
$"startsWith(displayName,'{escapedQuery}') or startsWith(mail,'{escapedQuery}') or startsWith(userPrincipalName,'{escapedQuery}')";
|
||||||
|
config.QueryParameters.Select = new[] { "displayName", "userPrincipalName", "mail" };
|
||||||
|
config.QueryParameters.Top = maxResults;
|
||||||
|
config.QueryParameters.Orderby = new[] { "displayName" };
|
||||||
|
config.Headers.Add("ConsistencyLevel", "eventual");
|
||||||
|
config.QueryParameters.Count = true;
|
||||||
|
}, ct);
|
||||||
|
|
||||||
|
if (response?.Value is null)
|
||||||
|
return Array.Empty<GraphUserResult>();
|
||||||
|
|
||||||
|
return response.Value
|
||||||
|
.Select(u => new GraphUserResult(
|
||||||
|
DisplayName: u.DisplayName ?? u.UserPrincipalName ?? "Unknown",
|
||||||
|
UserPrincipalName: u.UserPrincipalName ?? string.Empty,
|
||||||
|
Mail: u.Mail))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user