Files
RealCV/src/RealCV.Infrastructure/Clients/GitHubClient.cs
Peter Foster 5d2965beae feat: Add additional verification APIs (FCA, SRA, GitHub, OpenCorporates, ORCID)
This adds five new free API integrations for enhanced CV verification:

- FCA Register API: Verify financial services professionals
- SRA Register API: Verify solicitors and legal professionals
- GitHub API: Verify developer profiles and technical skills
- OpenCorporates API: Verify international companies across jurisdictions
- ORCID API: Verify academic researchers and publications

Includes:
- API clients for all five services with retry policies
- Service implementations with name matching and validation
- Models for verification results with detailed flags
- Configuration options in appsettings.json
- DI registration for all services

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 13:05:52 +00:00

266 lines
7.3 KiB
C#

using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace RealCV.Infrastructure.Clients;
public sealed class GitHubApiClient
{
private readonly HttpClient _httpClient;
private readonly ILogger<GitHubApiClient> _logger;
public GitHubApiClient(
HttpClient httpClient,
IOptions<GitHubOptions> options,
ILogger<GitHubApiClient> logger)
{
_httpClient = httpClient;
_logger = logger;
_httpClient.BaseAddress = new Uri("https://api.github.com/");
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
_httpClient.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28");
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("RealCV/1.0");
if (!string.IsNullOrEmpty(options.Value.PersonalAccessToken))
{
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", options.Value.PersonalAccessToken);
}
}
public async Task<GitHubUser?> GetUserAsync(string username)
{
try
{
var url = $"users/{Uri.EscapeDataString(username)}";
_logger.LogDebug("Getting GitHub user: {Username}", username);
var response = await _httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning("GitHub API returned {StatusCode} for user: {Username}",
response.StatusCode, username);
return null;
}
return await response.Content.ReadFromJsonAsync<GitHubUser>(JsonOptions);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting GitHub user: {Username}", username);
return null;
}
}
public async Task<List<GitHubRepo>> GetUserReposAsync(string username, int perPage = 100)
{
var repos = new List<GitHubRepo>();
var page = 1;
try
{
while (true)
{
var url = $"users/{Uri.EscapeDataString(username)}/repos?per_page={perPage}&page={page}&sort=updated";
var response = await _httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
break;
}
var pageRepos = await response.Content.ReadFromJsonAsync<List<GitHubRepo>>(JsonOptions);
if (pageRepos == null || pageRepos.Count == 0)
{
break;
}
repos.AddRange(pageRepos);
if (pageRepos.Count < perPage)
{
break;
}
page++;
// Limit to avoid rate limiting
if (page > 5)
{
break;
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting repos for user: {Username}", username);
}
return repos;
}
public async Task<Dictionary<string, int>?> GetRepoLanguagesAsync(string owner, string repo)
{
try
{
var url = $"repos/{Uri.EscapeDataString(owner)}/{Uri.EscapeDataString(repo)}/languages";
var response = await _httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
return null;
}
return await response.Content.ReadFromJsonAsync<Dictionary<string, int>>(JsonOptions);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting languages for repo: {Owner}/{Repo}", owner, repo);
return null;
}
}
public async Task<GitHubUserSearchResponse?> SearchUsersAsync(string query, int perPage = 30)
{
try
{
var encodedQuery = Uri.EscapeDataString(query);
var url = $"search/users?q={encodedQuery}&per_page={perPage}";
var response = await _httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
return null;
}
return await response.Content.ReadFromJsonAsync<GitHubUserSearchResponse>(JsonOptions);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error searching GitHub users: {Query}", query);
return null;
}
}
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
}
public class GitHubOptions
{
public string PersonalAccessToken { get; set; } = string.Empty;
}
// Response models
public class GitHubUser
{
public string? Login { get; set; }
public int Id { get; set; }
public string? Name { get; set; }
public string? Company { get; set; }
public string? Blog { get; set; }
public string? Location { get; set; }
public string? Email { get; set; }
public string? Bio { get; set; }
[JsonPropertyName("twitter_username")]
public string? TwitterUsername { get; set; }
[JsonPropertyName("public_repos")]
public int PublicRepos { get; set; }
[JsonPropertyName("public_gists")]
public int PublicGists { get; set; }
public int Followers { get; set; }
public int Following { get; set; }
[JsonPropertyName("created_at")]
public DateTime CreatedAt { get; set; }
[JsonPropertyName("updated_at")]
public DateTime UpdatedAt { get; set; }
[JsonPropertyName("html_url")]
public string? HtmlUrl { get; set; }
[JsonPropertyName("avatar_url")]
public string? AvatarUrl { get; set; }
}
public class GitHubRepo
{
public int Id { get; set; }
public string? Name { get; set; }
[JsonPropertyName("full_name")]
public string? FullName { get; set; }
public string? Description { get; set; }
public string? Language { get; set; }
[JsonPropertyName("html_url")]
public string? HtmlUrl { get; set; }
public bool Fork { get; set; }
public bool Private { get; set; }
[JsonPropertyName("stargazers_count")]
public int StargazersCount { get; set; }
[JsonPropertyName("watchers_count")]
public int WatchersCount { get; set; }
[JsonPropertyName("forks_count")]
public int ForksCount { get; set; }
public int Size { get; set; }
[JsonPropertyName("created_at")]
public DateTime CreatedAt { get; set; }
[JsonPropertyName("updated_at")]
public DateTime UpdatedAt { get; set; }
[JsonPropertyName("pushed_at")]
public DateTime? PushedAt { get; set; }
}
public class GitHubUserSearchResponse
{
[JsonPropertyName("total_count")]
public int TotalCount { get; set; }
[JsonPropertyName("incomplete_results")]
public bool IncompleteResults { get; set; }
public List<GitHubUserSearchItem>? Items { get; set; }
}
public class GitHubUserSearchItem
{
public string? Login { get; set; }
public int Id { get; set; }
[JsonPropertyName("avatar_url")]
public string? AvatarUrl { get; set; }
[JsonPropertyName("html_url")]
public string? HtmlUrl { get; set; }
public double Score { get; set; }
}