Files
RealCV/src/RealCV.Infrastructure/Services/GitHubVerifierService.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

276 lines
9.1 KiB
C#

using Microsoft.Extensions.Logging;
using RealCV.Application.Interfaces;
using RealCV.Application.Models;
using RealCV.Infrastructure.Clients;
namespace RealCV.Infrastructure.Services;
public sealed class GitHubVerifierService : IGitHubVerifierService
{
private readonly GitHubApiClient _gitHubClient;
private readonly ILogger<GitHubVerifierService> _logger;
// Map common skill names to GitHub languages
private static readonly Dictionary<string, string[]> SkillToLanguageMap = new(StringComparer.OrdinalIgnoreCase)
{
["JavaScript"] = ["JavaScript", "TypeScript"],
["TypeScript"] = ["TypeScript"],
["Python"] = ["Python"],
["Java"] = ["Java", "Kotlin"],
["C#"] = ["C#"],
[".NET"] = ["C#", "F#"],
["React"] = ["JavaScript", "TypeScript"],
["Angular"] = ["TypeScript", "JavaScript"],
["Vue"] = ["JavaScript", "TypeScript", "Vue"],
["Node.js"] = ["JavaScript", "TypeScript"],
["Go"] = ["Go"],
["Golang"] = ["Go"],
["Rust"] = ["Rust"],
["Ruby"] = ["Ruby"],
["PHP"] = ["PHP"],
["Swift"] = ["Swift"],
["Kotlin"] = ["Kotlin"],
["C++"] = ["C++", "C"],
["C"] = ["C"],
["Scala"] = ["Scala"],
["R"] = ["R"],
["SQL"] = ["PLSQL", "TSQL"],
["Shell"] = ["Shell", "Bash", "PowerShell"],
["DevOps"] = ["Shell", "Dockerfile", "HCL"],
["Docker"] = ["Dockerfile"],
["Terraform"] = ["HCL"],
["Mobile"] = ["Swift", "Kotlin", "Dart", "Java"],
["iOS"] = ["Swift", "Objective-C"],
["Android"] = ["Kotlin", "Java"],
["Flutter"] = ["Dart"],
["Machine Learning"] = ["Python", "Jupyter Notebook", "R"],
["Data Science"] = ["Python", "Jupyter Notebook", "R"],
};
public GitHubVerifierService(
GitHubApiClient gitHubClient,
ILogger<GitHubVerifierService> logger)
{
_gitHubClient = gitHubClient;
_logger = logger;
}
public async Task<GitHubVerificationResult> VerifyProfileAsync(string username)
{
try
{
_logger.LogInformation("Verifying GitHub profile: {Username}", username);
var user = await _gitHubClient.GetUserAsync(username);
if (user == null)
{
return new GitHubVerificationResult
{
ClaimedUsername = username,
IsVerified = false,
VerificationNotes = "GitHub profile not found"
};
}
// Get repositories for language analysis
var repos = await _gitHubClient.GetUserReposAsync(username);
// Analyze languages
var languageStats = new Dictionary<string, int>();
foreach (var repo in repos.Where(r => !r.Fork && !string.IsNullOrEmpty(r.Language)))
{
if (!languageStats.ContainsKey(repo.Language!))
languageStats[repo.Language!] = 0;
languageStats[repo.Language!]++;
}
// Calculate flags
var flags = new List<GitHubVerificationFlag>();
// Check account age
var accountAge = DateTime.UtcNow - user.CreatedAt;
if (accountAge.TotalDays < 90)
{
flags.Add(new GitHubVerificationFlag
{
Type = "NewAccount",
Severity = "Info",
Message = "Account created less than 90 days ago",
ScoreImpact = -5
});
}
// Check for empty profile
if (user.PublicRepos == 0)
{
flags.Add(new GitHubVerificationFlag
{
Type = "NoRepos",
Severity = "Warning",
Message = "No public repositories",
ScoreImpact = -10
});
}
return new GitHubVerificationResult
{
ClaimedUsername = username,
IsVerified = true,
ProfileName = user.Name,
ProfileUrl = user.HtmlUrl,
Bio = user.Bio,
Company = user.Company,
Location = user.Location,
AccountCreated = user.CreatedAt != default
? DateOnly.FromDateTime(user.CreatedAt)
: null,
PublicRepos = user.PublicRepos,
Followers = user.Followers,
Following = user.Following,
LanguageStats = languageStats,
VerificationNotes = BuildVerificationSummary(user, repos),
Flags = flags
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error verifying GitHub profile: {Username}", username);
return new GitHubVerificationResult
{
ClaimedUsername = username,
IsVerified = false,
VerificationNotes = $"Error during verification: {ex.Message}"
};
}
}
public async Task<GitHubVerificationResult> VerifySkillsAsync(
string username,
List<string> claimedSkills)
{
var result = await VerifyProfileAsync(username);
if (!result.IsVerified)
return result;
var skillVerifications = new List<SkillVerification>();
foreach (var skill in claimedSkills)
{
var verified = false;
var repoCount = 0;
// Check if the skill matches a known language directly
if (result.LanguageStats.TryGetValue(skill, out var count))
{
verified = true;
repoCount = count;
}
else if (SkillToLanguageMap.TryGetValue(skill, out var mappedLanguages))
{
// Check if any mapped language exists in the user's repos
foreach (var lang in mappedLanguages)
{
if (result.LanguageStats.TryGetValue(lang, out var langCount))
{
verified = true;
repoCount += langCount;
}
}
}
skillVerifications.Add(new SkillVerification
{
ClaimedSkill = skill,
IsVerified = verified,
RepoCount = repoCount,
Notes = verified
? $"Found in {repoCount} repositories"
: "No repositories found using this skill"
});
}
var verifiedCount = skillVerifications.Count(sv => sv.IsVerified);
var totalCount = skillVerifications.Count;
var percentage = totalCount > 0 ? (verifiedCount * 100) / totalCount : 0;
return result with
{
SkillVerifications = skillVerifications,
VerificationNotes = $"Skills verified: {verifiedCount}/{totalCount} ({percentage}%)"
};
}
public async Task<List<GitHubProfileSearchResult>> SearchProfilesAsync(string name)
{
try
{
var searchResponse = await _gitHubClient.SearchUsersAsync(name);
if (searchResponse?.Items == null)
{
return [];
}
var results = new List<GitHubProfileSearchResult>();
foreach (var item in searchResponse.Items.Take(10))
{
if (string.IsNullOrEmpty(item.Login))
continue;
// Get full profile details
var user = await _gitHubClient.GetUserAsync(item.Login);
results.Add(new GitHubProfileSearchResult
{
Username = item.Login,
Name = user?.Name,
AvatarUrl = item.AvatarUrl,
Bio = user?.Bio,
PublicRepos = user?.PublicRepos ?? 0,
Followers = user?.Followers ?? 0
});
}
return results;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error searching GitHub profiles: {Name}", name);
return [];
}
}
private static string BuildVerificationSummary(GitHubUser user, List<GitHubRepo> repos)
{
var parts = new List<string>
{
$"Account created: {user.CreatedAt:yyyy-MM-dd}",
$"Public repos: {user.PublicRepos}",
$"Followers: {user.Followers}"
};
var totalStars = repos.Sum(r => r.StargazersCount);
if (totalStars > 0)
{
parts.Add($"Total stars: {totalStars}");
}
var topLanguages = repos
.Where(r => !r.Fork && !string.IsNullOrEmpty(r.Language))
.GroupBy(r => r.Language)
.OrderByDescending(g => g.Count())
.Take(3)
.Select(g => g.Key);
if (topLanguages.Any())
{
parts.Add($"Top languages: {string.Join(", ", topLanguages)}");
}
return string.Join(" | ", parts);
}
}