diff --git a/SharepointToolbox/Services/GraphUserSearchService.cs b/SharepointToolbox/Services/GraphUserSearchService.cs new file mode 100644 index 0000000..3af13ec --- /dev/null +++ b/SharepointToolbox/Services/GraphUserSearchService.cs @@ -0,0 +1,53 @@ +using SharepointToolbox.Infrastructure.Auth; + +namespace SharepointToolbox.Services; + +/// +/// Searches tenant users via Microsoft Graph API. +/// Used by the people-picker autocomplete in the User Access Audit tab. +/// +public class GraphUserSearchService : IGraphUserSearchService +{ + private readonly GraphClientFactory _graphClientFactory; + + public GraphUserSearchService(GraphClientFactory graphClientFactory) + { + _graphClientFactory = graphClientFactory; + } + + public async Task> SearchUsersAsync( + string clientId, + string query, + int maxResults = 10, + CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(query) || query.Length < 2) + return Array.Empty(); + + 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(); + + return response.Value + .Select(u => new GraphUserResult( + DisplayName: u.DisplayName ?? u.UserPrincipalName ?? "Unknown", + UserPrincipalName: u.UserPrincipalName ?? string.Empty, + Mail: u.Mail)) + .ToList(); + } +}