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>
This commit is contained in:
275
src/RealCV.Infrastructure/Services/GitHubVerifierService.cs
Normal file
275
src/RealCV.Infrastructure/Services/GitHubVerifierService.cs
Normal file
@@ -0,0 +1,275 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user