From a926c214e1351cfd8d710b0fe6061d9c40165bed Mon Sep 17 00:00:00 2001 From: Gelu Mihes Date: Wed, 6 May 2026 15:26:25 +0300 Subject: [PATCH] Changes --- api/Controllers/CaptchaController.cs | 2 +- api/Controllers/ContactController.cs | 4 +- api/Controllers/CvMatcherController.cs | 4 +- api/Models/Requests/CaptchaVerifyRequest.cs | 1 + api/Program.cs | 14 ------- api/Services/Contracts/ICaptchaVerifier.cs | 2 +- api/Services/RecaptchaVerifier.cs | 9 +++-- cv-matcher-api/Program.cs | 14 ------- rag-api/Program.cs | 14 ------- web/wwwroot/js/main.js | 42 ++++++++++++++------- 10 files changed, 40 insertions(+), 66 deletions(-) diff --git a/api/Controllers/CaptchaController.cs b/api/Controllers/CaptchaController.cs index 84b0c55..4ac7520 100644 --- a/api/Controllers/CaptchaController.cs +++ b/api/Controllers/CaptchaController.cs @@ -48,7 +48,7 @@ namespace Api.Controllers if (req is null || string.IsNullOrWhiteSpace(req.Token)) return BadRequest(new { error = "Missing token" }); var userIp = HttpContext.Connection.RemoteIpAddress?.ToString(); - var verdict = await _captcha.VerifyAsync(req.Token, userIp, ct); + var verdict = await _captcha.VerifyAsync(req.Token, userIp, req.ExpectedAction, ct); if (!verdict.Success) { _log.LogWarning("Captcha failed. ip={Ip} score={Score} err={Err}", userIp, verdict.Score, verdict.Error); diff --git a/api/Controllers/ContactController.cs b/api/Controllers/ContactController.cs index dd49cd6..cc2fea6 100644 --- a/api/Controllers/ContactController.cs +++ b/api/Controllers/ContactController.cs @@ -48,7 +48,7 @@ namespace Api.Controllers return ValidationProblem(ModelState); var userIp = HttpContext.Connection.RemoteIpAddress?.ToString(); - var verdict = await _captcha.VerifyAsync(req.CaptchaToken, userIp, ct); + var verdict = await _captcha.VerifyAsync(req.CaptchaToken, userIp, "contact", ct); if (!verdict.Success) return BadRequest("Captcha verification failed."); try @@ -82,7 +82,7 @@ namespace Api.Controllers return ValidationProblem(ModelState); var userIp = HttpContext.Connection.RemoteIpAddress?.ToString(); - var verdict = await _captcha.VerifyAsync(req.CaptchaToken, userIp, ct); + var verdict = await _captcha.VerifyAsync(req.CaptchaToken, userIp, "contact", ct); if (!verdict.Success) return BadRequest("Captcha verification failed."); try diff --git a/api/Controllers/CvMatcherController.cs b/api/Controllers/CvMatcherController.cs index 7d31bbb..4542176 100644 --- a/api/Controllers/CvMatcherController.cs +++ b/api/Controllers/CvMatcherController.cs @@ -65,7 +65,7 @@ public sealed class CvMatcherController : ControllerBase cv.FileName, cv.Length, gdprConsent); var userIp = HttpContext.Connection.RemoteIpAddress?.ToString(); - var verdict = await _captcha.VerifyAsync(request.CaptchaToken ?? string.Empty, userIp, ct); + var verdict = await _captcha.VerifyAsync(request.CaptchaToken ?? string.Empty, userIp, "cv_upload", ct); if (!verdict.Success) { _logger.LogWarning("Captcha verification failed for CV upload. IP={IP}", userIp); @@ -112,7 +112,7 @@ public sealed class CvMatcherController : ControllerBase !string.IsNullOrWhiteSpace(request.JobDescription)); var userIp = HttpContext.Connection.RemoteIpAddress?.ToString(); - var verdict = await _captcha.VerifyAsync(request.CaptchaToken ?? string.Empty, userIp, ct); + var verdict = await _captcha.VerifyAsync(request.CaptchaToken ?? string.Empty, userIp, "match_job", ct); if (!verdict.Success) { _logger.LogWarning("Captcha verification failed for job match. IP={IP}", userIp); diff --git a/api/Models/Requests/CaptchaVerifyRequest.cs b/api/Models/Requests/CaptchaVerifyRequest.cs index 836b348..b8ce081 100644 --- a/api/Models/Requests/CaptchaVerifyRequest.cs +++ b/api/Models/Requests/CaptchaVerifyRequest.cs @@ -3,6 +3,7 @@ public class CaptchaVerifyRequest { public string? Token { get; set; } + public string? ExpectedAction { get; set; } } } diff --git a/api/Program.cs b/api/Program.cs index 6d4bb07..14e4b9b 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -299,9 +299,6 @@ finally Log.CloseAndFlush(); } -/// -/// Logs all environment variables and configuration settings at startup for diagnostics. -/// static void LogEnvironmentSettings(Microsoft.Extensions.Logging.ILogger logger, IConfiguration configuration, IWebHostEnvironment environment) { logger.LogInformation("==================== ENVIRONMENT SETTINGS ===================="); @@ -343,9 +340,6 @@ static void LogEnvironmentSettings(Microsoft.Extensions.Logging.ILogger logger, logger.LogInformation("==========================================================="); } -/// -/// Recursively logs configuration settings with hierarchy. -/// static void LogConfigurationRecursive(Microsoft.Extensions.Logging.ILogger logger, IEnumerable sections, string prefix) { foreach (var section in sections) @@ -373,9 +367,6 @@ static void LogConfigurationRecursive(Microsoft.Extensions.Logging.ILogger logge } } -/// -/// Checks if a configuration key contains sensitive information. -/// static bool IsSensitiveKey(string key) { return key.Contains("Password", StringComparison.OrdinalIgnoreCase) || @@ -385,11 +376,6 @@ static bool IsSensitiveKey(string key) key.Contains("ConnectionString", StringComparison.OrdinalIgnoreCase); } -/// -/// Masks a sensitive value but shows the last 4 characters for verification. -/// -/// The value to mask. -/// Masked value showing last 4 characters (e.g., "***MASKED***...abcd") static string MaskValueWithLastChars(string value) { if (string.IsNullOrEmpty(value)) diff --git a/api/Services/Contracts/ICaptchaVerifier.cs b/api/Services/Contracts/ICaptchaVerifier.cs index 55e7649..a97754c 100644 --- a/api/Services/Contracts/ICaptchaVerifier.cs +++ b/api/Services/Contracts/ICaptchaVerifier.cs @@ -4,6 +4,6 @@ namespace Api.Services.Contracts { public interface ICaptchaVerifier { - Task VerifyAsync(string token, string? userIp, CancellationToken ct); + Task VerifyAsync(string token, string? userIp, string? expectedAction, CancellationToken ct); } } diff --git a/api/Services/RecaptchaVerifier.cs b/api/Services/RecaptchaVerifier.cs index 2109a55..975fdb0 100644 --- a/api/Services/RecaptchaVerifier.cs +++ b/api/Services/RecaptchaVerifier.cs @@ -18,7 +18,7 @@ namespace Api.Services _log = log; } - public async Task VerifyAsync(string token, string? userIp, CancellationToken ct) + public async Task VerifyAsync(string token, string? userIp, string? expectedAction, CancellationToken ct) { _log.LogDebug("Verifying captcha token for IP {Ip}", userIp ?? "unknown"); @@ -72,11 +72,12 @@ namespace Api.Services } // Optional strictness (usually v3): action/hostname checks - if (!string.IsNullOrWhiteSpace(_opt.ExpectedAction) && - !string.Equals(_opt.ExpectedAction, data.action, StringComparison.Ordinal)) + 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}", - _opt.ExpectedAction, data.action, userIp ?? "unknown"); + actionToCheck, data.action, userIp ?? "unknown"); return new CaptchaVerdictModel(false, "Captcha action mismatch", data.score); } diff --git a/cv-matcher-api/Program.cs b/cv-matcher-api/Program.cs index c5fda2b..6a43f87 100644 --- a/cv-matcher-api/Program.cs +++ b/cv-matcher-api/Program.cs @@ -213,9 +213,6 @@ finally Log.CloseAndFlush(); } -/// -/// Logs all environment variables and configuration settings at startup for diagnostics. -/// static void LogEnvironmentSettings(Microsoft.Extensions.Logging.ILogger logger, IConfiguration configuration, IWebHostEnvironment environment) { logger.LogInformation("==================== ENVIRONMENT SETTINGS ===================="); @@ -257,9 +254,6 @@ static void LogEnvironmentSettings(Microsoft.Extensions.Logging.ILogger logger, logger.LogInformation("==========================================================="); } -/// -/// Recursively logs configuration settings with hierarchy. -/// static void LogConfigurationRecursive(Microsoft.Extensions.Logging.ILogger logger, IEnumerable sections, string prefix) { foreach (var section in sections) @@ -287,9 +281,6 @@ static void LogConfigurationRecursive(Microsoft.Extensions.Logging.ILogger logge } } -/// -/// Checks if a configuration key contains sensitive information. -/// static bool IsSensitiveKey(string key) { return key.Contains("Password", StringComparison.OrdinalIgnoreCase) || @@ -299,11 +290,6 @@ static bool IsSensitiveKey(string key) key.Contains("ConnectionString", StringComparison.OrdinalIgnoreCase); } -/// -/// Masks a sensitive value but shows the last 4 characters for verification. -/// -/// The value to mask. -/// Masked value showing last 4 characters (e.g., "***MASKED***...abcd") static string MaskValueWithLastChars(string value) { if (string.IsNullOrEmpty(value)) diff --git a/rag-api/Program.cs b/rag-api/Program.cs index 009458f..b71bf5d 100644 --- a/rag-api/Program.cs +++ b/rag-api/Program.cs @@ -195,9 +195,6 @@ finally Log.CloseAndFlush(); } -/// -/// Logs all environment variables and configuration settings at startup for diagnostics. -/// static void LogEnvironmentSettings(Microsoft.Extensions.Logging.ILogger logger, IConfiguration configuration, IWebHostEnvironment environment) { logger.LogInformation("==================== ENVIRONMENT SETTINGS ===================="); @@ -239,9 +236,6 @@ static void LogEnvironmentSettings(Microsoft.Extensions.Logging.ILogger logger, logger.LogInformation("==========================================================="); } -/// -/// Recursively logs configuration settings with hierarchy. -/// static void LogConfigurationRecursive(Microsoft.Extensions.Logging.ILogger logger, IEnumerable sections, string prefix) { foreach (var section in sections) @@ -269,9 +263,6 @@ static void LogConfigurationRecursive(Microsoft.Extensions.Logging.ILogger logge } } -/// -/// Checks if a configuration key contains sensitive information. -/// static bool IsSensitiveKey(string key) { return key.Contains("Password", StringComparison.OrdinalIgnoreCase) || @@ -281,11 +272,6 @@ static bool IsSensitiveKey(string key) key.Contains("ConnectionString", StringComparison.OrdinalIgnoreCase); } -/// -/// Masks a sensitive value but shows the last 4 characters for verification. -/// -/// The value to mask. -/// Masked value showing last 4 characters (e.g., "***MASKED***...abcd") static string MaskValueWithLastChars(string value) { if (string.IsNullOrEmpty(value)) diff --git a/web/wwwroot/js/main.js b/web/wwwroot/js/main.js index 78429e9..e983b13 100644 --- a/web/wwwroot/js/main.js +++ b/web/wwwroot/js/main.js @@ -461,7 +461,7 @@ if (window.grecaptcha && reCaptchaSiteKey) { grecaptcha.ready(function () { grecaptcha.execute(reCaptchaSiteKey, { - action: 'contact' + action: 'cv_upload' }).then(postCv); }); } else { @@ -470,7 +470,7 @@ $button.prop('disabled', false).text(t('cv.submit')); return; } - function postCv(token) { + async function postCv(token) { try { var formData = new FormData(); formData.append('cv', file); @@ -482,19 +482,33 @@ }); if (!cvResponse.ok) throw new Error(t('cv.cvFailed')); var cvData = await cvResponse.json(); + // Before calling match, obtain a fresh captcha token for the match action + if (!(window.grecaptcha && reCaptchaSiteKey)) { + throw new Error(t('form.captchaFailed')); + } + + // get match token + var matchToken = await new Promise(function (resolve, reject) { + try { + grecaptcha.ready(function () { + grecaptcha.execute(reCaptchaSiteKey, { action: 'match_job' }).then(resolve).catch(reject); + }); + } catch (e) { reject(e); } + }); + var matchResponse = await fetch('/api/cv-matcher/match-job', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - cvDocumentId: cvData.documentId || cvData.cvDocumentId, - jobUrl: jobUrl, - jobDescription: jobDescription, - gdprConsent: consent, - captchaToken: token - }) - }); + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + cvDocumentId: cvData.documentId || cvData.cvDocumentId, + jobUrl: jobUrl, + jobDescription: jobDescription, + gdprConsent: consent, + captchaToken: matchToken + }) + }); if (!matchResponse.ok) throw new Error(t('cv.matchFailed')); var match = await matchResponse.json(); renderMatchResult(match);