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>
276 lines
9.1 KiB
C#
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);
|
|
}
|
|
}
|