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);