@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
+28
-14
@@ -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,19 +482,33 @@
|
|||||||
});
|
});
|
||||||
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: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
cvDocumentId: cvData.documentId || cvData.cvDocumentId,
|
cvDocumentId: cvData.documentId || cvData.cvDocumentId,
|
||||||
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'));
|
||||||
var match = await matchResponse.json();
|
var match = await matchResponse.json();
|
||||||
renderMatchResult(match);
|
renderMatchResult(match);
|
||||||
|
|||||||
Reference in New Issue
Block a user