Files
myAi/Apis/api/Services/RecaptchaVerifier.cs
claude 16bb195cb5 Add XML doc to all service interfaces and implementations (#26)
- Update CLAUDE.md: replace incorrect 'no XML doc on internal code' rule
  with the correct convention (XML doc on all public methods and
  non-trivial private/protected helpers)
- Restore /// <summary> on FileDownloadController private helpers
  (HandleRangeRequest, StreamRangeAsync)
- Add full XML doc to all service contracts:
  ICaptchaVerifier, IEmailSender, ICvMatcherService, IJobTextExtractor,
  IJobTokenService, IDocumentClassifier, IRagService, ITextChunker,
  ITextExtractor, IEmailTemplateService, ITemplateService
- Add /// <summary> and /// <inheritdoc /> to all concrete service classes
  and their methods: RecaptchaVerifier, EmailApiEmailSender,
  SmtpEmailDispatcher, CvMatcherService, JobTextExtractor, JobTokenService,
  RagService, DocumentClassifier, TextChunker, TextExtractor,
  HtmlJobSearcher, CvSearchEmailSender, CvSearchJobTask,
  EmailTemplateService, DbTemplateService

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 09:17:42 +03:00

111 lines
4.7 KiB
C#

using Api.Services.Contracts.Models;
using Api.Services.Contracts;
using Microsoft.Extensions.Options;
using Models.Settings;
namespace Api.Services
{
/// <summary>
/// Verifies reCAPTCHA v2/v3 tokens by calling the Google site-verify API.
/// </summary>
public sealed class RecaptchaVerifier : ICaptchaVerifier
{
private readonly HttpClient _http;
private readonly CaptchaSettings _opt;
private readonly ILogger<RecaptchaVerifier> _log;
public RecaptchaVerifier(HttpClient http, IOptions<CaptchaSettings> options, ILogger<RecaptchaVerifier> log)
{
_http = http;
_opt = options.Value;
_log = log;
}
/// <inheritdoc />
public async Task<CaptchaVerdictModel> VerifyAsync(string token, string? userIp, string? expectedAction, CancellationToken ct)
{
_log.LogDebug("Verifying captcha token for IP {Ip}", userIp ?? "unknown");
if (string.IsNullOrWhiteSpace(_opt.SecretKey))
{
_log.LogWarning("Captcha verification attempted but SecretKey is not configured");
return new CaptchaVerdictModel(false, "Captcha not configured", null);
}
var form = new Dictionary<string, string>
{
["secret"] = _opt.SecretKey,
["response"] = token
};
if (!string.IsNullOrWhiteSpace(userIp))
form["remoteip"] = userIp;
using var resp = await _http.PostAsync(
"https://www.google.com/recaptcha/api/siteverify",
new FormUrlEncodedContent(form),
ct
);
if (!resp.IsSuccessStatusCode)
{
_log.LogWarning("Captcha HTTP request failed with status {StatusCode} for IP {Ip}",
(int)resp.StatusCode, userIp ?? "unknown");
return new CaptchaVerdictModel(false, $"Captcha HTTP {(int)resp.StatusCode}", null);
}
var data = await resp.Content.ReadFromJsonAsync<RecaptchaResponse>(cancellationToken: ct);
if (data is null)
{
_log.LogError("Failed to parse captcha response for IP {Ip}", userIp ?? "unknown");
return new CaptchaVerdictModel(false, "Captcha parse error", null);
}
if (!data.success)
{
_log.LogWarning("Captcha verification failed for IP {Ip}. Score={Score}",
userIp ?? "unknown", data.score);
return new CaptchaVerdictModel(false, "Captcha failed", data.score);
}
// v3 score check (score is typically null for v2)
if (data.score is double score && score < _opt.MinimumScore)
{
_log.LogWarning("Captcha score {Score} below minimum {MinScore} for IP {Ip}",
score, _opt.MinimumScore, userIp ?? "unknown");
return new CaptchaVerdictModel(false, "Captcha score too low", score);
}
// Optional strictness (usually v3): action/hostname checks
var actionToCheck = !string.IsNullOrWhiteSpace(expectedAction) ? expectedAction : _opt.ExpectedAction;
if (!string.IsNullOrWhiteSpace(actionToCheck) &&
!string.Equals(actionToCheck, data.action, StringComparison.Ordinal))
{
_log.LogWarning("Captcha action mismatch. Expected={Expected}, Actual={Actual}, IP={Ip}",
actionToCheck, data.action, userIp ?? "unknown");
return new CaptchaVerdictModel(false, "Captcha action mismatch", data.score);
}
if (!string.IsNullOrWhiteSpace(_opt.ExpectedHostname) &&
!string.Equals(_opt.ExpectedHostname, data.hostname, StringComparison.OrdinalIgnoreCase))
{
_log.LogWarning("Captcha hostname mismatch. Expected={Expected}, Actual={Actual}, IP={Ip}",
_opt.ExpectedHostname, data.hostname, userIp ?? "unknown");
return new CaptchaVerdictModel(false, "Captcha hostname mismatch", data.score);
}
_log.LogInformation("Captcha verified successfully for IP {Ip}. Score={Score}",
userIp ?? "unknown", data.score);
return new CaptchaVerdictModel(true, null, data.score);
}
private sealed class RecaptchaResponse
{
public bool success { get; set; }
public double? score { get; set; } // v3
public string? action { get; set; } // v3
public string? hostname { get; set; }
public DateTimeOffset? challenge_ts { get; set; }
}
}
}