Changes
Build and Push Docker Images / build (push) Successful in 28s

This commit is contained in:
2026-05-06 15:26:25 +03:00
parent a10908364b
commit a926c214e1
10 changed files with 40 additions and 66 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ namespace Api.Controllers
if (req is null || string.IsNullOrWhiteSpace(req.Token)) return BadRequest(new { error = "Missing token" }); if (req is null || string.IsNullOrWhiteSpace(req.Token)) return BadRequest(new { error = "Missing token" });
var userIp = HttpContext.Connection.RemoteIpAddress?.ToString(); 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) if (!verdict.Success)
{ {
_log.LogWarning("Captcha failed. ip={Ip} score={Score} err={Err}", userIp, verdict.Score, verdict.Error); _log.LogWarning("Captcha failed. ip={Ip} score={Score} err={Err}", userIp, verdict.Score, verdict.Error);
+2 -2
View File
@@ -48,7 +48,7 @@ namespace Api.Controllers
return ValidationProblem(ModelState); return ValidationProblem(ModelState);
var userIp = HttpContext.Connection.RemoteIpAddress?.ToString(); 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."); if (!verdict.Success) return BadRequest("Captcha verification failed.");
try try
@@ -82,7 +82,7 @@ namespace Api.Controllers
return ValidationProblem(ModelState); return ValidationProblem(ModelState);
var userIp = HttpContext.Connection.RemoteIpAddress?.ToString(); 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."); if (!verdict.Success) return BadRequest("Captcha verification failed.");
try try
+2 -2
View File
@@ -65,7 +65,7 @@ public sealed class CvMatcherController : ControllerBase
cv.FileName, cv.Length, gdprConsent); cv.FileName, cv.Length, gdprConsent);
var userIp = HttpContext.Connection.RemoteIpAddress?.ToString(); 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) if (!verdict.Success)
{ {
_logger.LogWarning("Captcha verification failed for CV upload. IP={IP}", userIp); _logger.LogWarning("Captcha verification failed for CV upload. IP={IP}", userIp);
@@ -112,7 +112,7 @@ public sealed class CvMatcherController : ControllerBase
!string.IsNullOrWhiteSpace(request.JobDescription)); !string.IsNullOrWhiteSpace(request.JobDescription));
var userIp = HttpContext.Connection.RemoteIpAddress?.ToString(); 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) if (!verdict.Success)
{ {
_logger.LogWarning("Captcha verification failed for job match. IP={IP}", userIp); _logger.LogWarning("Captcha verification failed for job match. IP={IP}", userIp);
@@ -3,6 +3,7 @@
public class CaptchaVerifyRequest public class CaptchaVerifyRequest
{ {
public string? Token { get; set; } public string? Token { get; set; }
public string? ExpectedAction { get; set; }
} }
} }
-14
View File
@@ -299,9 +299,6 @@ finally
Log.CloseAndFlush(); Log.CloseAndFlush();
} }
/// <summary>
/// Logs all environment variables and configuration settings at startup for diagnostics.
/// </summary>
static void LogEnvironmentSettings(Microsoft.Extensions.Logging.ILogger logger, IConfiguration configuration, IWebHostEnvironment environment) static void LogEnvironmentSettings(Microsoft.Extensions.Logging.ILogger logger, IConfiguration configuration, IWebHostEnvironment environment)
{ {
logger.LogInformation("==================== ENVIRONMENT SETTINGS ===================="); logger.LogInformation("==================== ENVIRONMENT SETTINGS ====================");
@@ -343,9 +340,6 @@ static void LogEnvironmentSettings(Microsoft.Extensions.Logging.ILogger logger,
logger.LogInformation("==========================================================="); logger.LogInformation("===========================================================");
} }
/// <summary>
/// Recursively logs configuration settings with hierarchy.
/// </summary>
static void LogConfigurationRecursive(Microsoft.Extensions.Logging.ILogger logger, IEnumerable<IConfigurationSection> sections, string prefix) static void LogConfigurationRecursive(Microsoft.Extensions.Logging.ILogger logger, IEnumerable<IConfigurationSection> sections, string prefix)
{ {
foreach (var section in sections) foreach (var section in sections)
@@ -373,9 +367,6 @@ static void LogConfigurationRecursive(Microsoft.Extensions.Logging.ILogger logge
} }
} }
/// <summary>
/// Checks if a configuration key contains sensitive information.
/// </summary>
static bool IsSensitiveKey(string key) static bool IsSensitiveKey(string key)
{ {
return key.Contains("Password", StringComparison.OrdinalIgnoreCase) || return key.Contains("Password", StringComparison.OrdinalIgnoreCase) ||
@@ -385,11 +376,6 @@ static bool IsSensitiveKey(string key)
key.Contains("ConnectionString", StringComparison.OrdinalIgnoreCase); key.Contains("ConnectionString", StringComparison.OrdinalIgnoreCase);
} }
/// <summary>
/// Masks a sensitive value but shows the last 4 characters for verification.
/// </summary>
/// <param name="value">The value to mask.</param>
/// <returns>Masked value showing last 4 characters (e.g., "***MASKED***...abcd")</returns>
static string MaskValueWithLastChars(string value) static string MaskValueWithLastChars(string value)
{ {
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
+1 -1
View File
@@ -4,6 +4,6 @@ namespace Api.Services.Contracts
{ {
public interface ICaptchaVerifier public interface ICaptchaVerifier
{ {
Task<CaptchaVerdictModel> VerifyAsync(string token, string? userIp, CancellationToken ct); Task<CaptchaVerdictModel> VerifyAsync(string token, string? userIp, string? expectedAction, CancellationToken ct);
} }
} }
+5 -4
View File
@@ -18,7 +18,7 @@ namespace Api.Services
_log = log; _log = log;
} }
public async Task<CaptchaVerdictModel> VerifyAsync(string token, string? userIp, CancellationToken ct) public async Task<CaptchaVerdictModel> VerifyAsync(string token, string? userIp, string? expectedAction, CancellationToken ct)
{ {
_log.LogDebug("Verifying captcha token for IP {Ip}", userIp ?? "unknown"); _log.LogDebug("Verifying captcha token for IP {Ip}", userIp ?? "unknown");
@@ -72,11 +72,12 @@ namespace Api.Services
} }
// Optional strictness (usually v3): action/hostname checks // Optional strictness (usually v3): action/hostname checks
if (!string.IsNullOrWhiteSpace(_opt.ExpectedAction) && var actionToCheck = !string.IsNullOrWhiteSpace(expectedAction) ? expectedAction : _opt.ExpectedAction;
!string.Equals(_opt.ExpectedAction, data.action, StringComparison.Ordinal)) if (!string.IsNullOrWhiteSpace(actionToCheck) &&
!string.Equals(actionToCheck, data.action, StringComparison.Ordinal))
{ {
_log.LogWarning("Captcha action mismatch. Expected={Expected}, Actual={Actual}, IP={Ip}", _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); return new CaptchaVerdictModel(false, "Captcha action mismatch", data.score);
} }
-14
View File
@@ -213,9 +213,6 @@ finally
Log.CloseAndFlush(); Log.CloseAndFlush();
} }
/// <summary>
/// Logs all environment variables and configuration settings at startup for diagnostics.
/// </summary>
static void LogEnvironmentSettings(Microsoft.Extensions.Logging.ILogger logger, IConfiguration configuration, IWebHostEnvironment environment) static void LogEnvironmentSettings(Microsoft.Extensions.Logging.ILogger logger, IConfiguration configuration, IWebHostEnvironment environment)
{ {
logger.LogInformation("==================== ENVIRONMENT SETTINGS ===================="); logger.LogInformation("==================== ENVIRONMENT SETTINGS ====================");
@@ -257,9 +254,6 @@ static void LogEnvironmentSettings(Microsoft.Extensions.Logging.ILogger logger,
logger.LogInformation("==========================================================="); logger.LogInformation("===========================================================");
} }
/// <summary>
/// Recursively logs configuration settings with hierarchy.
/// </summary>
static void LogConfigurationRecursive(Microsoft.Extensions.Logging.ILogger logger, IEnumerable<IConfigurationSection> sections, string prefix) static void LogConfigurationRecursive(Microsoft.Extensions.Logging.ILogger logger, IEnumerable<IConfigurationSection> sections, string prefix)
{ {
foreach (var section in sections) foreach (var section in sections)
@@ -287,9 +281,6 @@ static void LogConfigurationRecursive(Microsoft.Extensions.Logging.ILogger logge
} }
} }
/// <summary>
/// Checks if a configuration key contains sensitive information.
/// </summary>
static bool IsSensitiveKey(string key) static bool IsSensitiveKey(string key)
{ {
return key.Contains("Password", StringComparison.OrdinalIgnoreCase) || return key.Contains("Password", StringComparison.OrdinalIgnoreCase) ||
@@ -299,11 +290,6 @@ static bool IsSensitiveKey(string key)
key.Contains("ConnectionString", StringComparison.OrdinalIgnoreCase); key.Contains("ConnectionString", StringComparison.OrdinalIgnoreCase);
} }
/// <summary>
/// Masks a sensitive value but shows the last 4 characters for verification.
/// </summary>
/// <param name="value">The value to mask.</param>
/// <returns>Masked value showing last 4 characters (e.g., "***MASKED***...abcd")</returns>
static string MaskValueWithLastChars(string value) static string MaskValueWithLastChars(string value)
{ {
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
-14
View File
@@ -195,9 +195,6 @@ finally
Log.CloseAndFlush(); Log.CloseAndFlush();
} }
/// <summary>
/// Logs all environment variables and configuration settings at startup for diagnostics.
/// </summary>
static void LogEnvironmentSettings(Microsoft.Extensions.Logging.ILogger logger, IConfiguration configuration, IWebHostEnvironment environment) static void LogEnvironmentSettings(Microsoft.Extensions.Logging.ILogger logger, IConfiguration configuration, IWebHostEnvironment environment)
{ {
logger.LogInformation("==================== ENVIRONMENT SETTINGS ===================="); logger.LogInformation("==================== ENVIRONMENT SETTINGS ====================");
@@ -239,9 +236,6 @@ static void LogEnvironmentSettings(Microsoft.Extensions.Logging.ILogger logger,
logger.LogInformation("==========================================================="); logger.LogInformation("===========================================================");
} }
/// <summary>
/// Recursively logs configuration settings with hierarchy.
/// </summary>
static void LogConfigurationRecursive(Microsoft.Extensions.Logging.ILogger logger, IEnumerable<IConfigurationSection> sections, string prefix) static void LogConfigurationRecursive(Microsoft.Extensions.Logging.ILogger logger, IEnumerable<IConfigurationSection> sections, string prefix)
{ {
foreach (var section in sections) foreach (var section in sections)
@@ -269,9 +263,6 @@ static void LogConfigurationRecursive(Microsoft.Extensions.Logging.ILogger logge
} }
} }
/// <summary>
/// Checks if a configuration key contains sensitive information.
/// </summary>
static bool IsSensitiveKey(string key) static bool IsSensitiveKey(string key)
{ {
return key.Contains("Password", StringComparison.OrdinalIgnoreCase) || return key.Contains("Password", StringComparison.OrdinalIgnoreCase) ||
@@ -281,11 +272,6 @@ static bool IsSensitiveKey(string key)
key.Contains("ConnectionString", StringComparison.OrdinalIgnoreCase); key.Contains("ConnectionString", StringComparison.OrdinalIgnoreCase);
} }
/// <summary>
/// Masks a sensitive value but shows the last 4 characters for verification.
/// </summary>
/// <param name="value">The value to mask.</param>
/// <returns>Masked value showing last 4 characters (e.g., "***MASKED***...abcd")</returns>
static string MaskValueWithLastChars(string value) static string MaskValueWithLastChars(string value)
{ {
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
+17 -3
View File
@@ -461,7 +461,7 @@
if (window.grecaptcha && reCaptchaSiteKey) { if (window.grecaptcha && reCaptchaSiteKey) {
grecaptcha.ready(function () { grecaptcha.ready(function () {
grecaptcha.execute(reCaptchaSiteKey, { grecaptcha.execute(reCaptchaSiteKey, {
action: 'contact' action: 'cv_upload'
}).then(postCv); }).then(postCv);
}); });
} else { } else {
@@ -470,7 +470,7 @@
$button.prop('disabled', false).text(t('cv.submit')); $button.prop('disabled', false).text(t('cv.submit'));
return; return;
} }
function postCv(token) { async function postCv(token) {
try { try {
var formData = new FormData(); var formData = new FormData();
formData.append('cv', file); formData.append('cv', file);
@@ -482,6 +482,20 @@
}); });
if (!cvResponse.ok) throw new Error(t('cv.cvFailed')); if (!cvResponse.ok) throw new Error(t('cv.cvFailed'));
var cvData = await cvResponse.json(); 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', { var matchResponse = await fetch('/api/cv-matcher/match-job', {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -492,7 +506,7 @@
jobUrl: jobUrl, jobUrl: jobUrl,
jobDescription: jobDescription, jobDescription: jobDescription,
gdprConsent: consent, gdprConsent: consent,
captchaToken: token captchaToken: matchToken
}) })
}); });
if (!matchResponse.ok) throw new Error(t('cv.matchFailed')); if (!matchResponse.ok) throw new Error(t('cv.matchFailed'));