Changes
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
using Api.Services.Contracts.Models;
|
|
||||||
using Api.Services.Contracts;
|
using Api.Services.Contracts;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Api.Models.Settings;
|
using Api.Models.Settings;
|
||||||
using Swashbuckle.AspNetCore.Annotations;
|
using Swashbuckle.AspNetCore.Annotations;
|
||||||
|
using Api.Models.Requests;
|
||||||
|
|
||||||
namespace Api.Controllers
|
namespace Api.Controllers
|
||||||
{
|
{
|
||||||
@@ -29,7 +29,7 @@ namespace Api.Controllers
|
|||||||
/// Returns the public reCAPTCHA site key used by the client to render the widget.
|
/// Returns the public reCAPTCHA site key used by the client to render the widget.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[SwaggerOperation(Summary = "Get reCAPTCHA site key")]
|
[SwaggerOperation(Summary = "Get captcha site key")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public IActionResult GetSiteKey()
|
public IActionResult GetSiteKey()
|
||||||
{
|
{
|
||||||
@@ -43,7 +43,7 @@ namespace Api.Controllers
|
|||||||
[SwaggerOperation(Summary = "Verify captcha token")]
|
[SwaggerOperation(Summary = "Verify captcha token")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
public async Task<IActionResult> Verify([FromBody] VerifyRequest req, CancellationToken ct)
|
public async Task<IActionResult> Verify([FromBody] CaptchaVerifyRequest req, CancellationToken ct)
|
||||||
{
|
{
|
||||||
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" });
|
||||||
|
|
||||||
@@ -57,10 +57,5 @@ namespace Api.Controllers
|
|||||||
|
|
||||||
return Ok(verdict);
|
return Ok(verdict);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class VerifyRequest
|
|
||||||
{
|
|
||||||
public string? Token { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,26 +19,16 @@ namespace Api.Controllers
|
|||||||
[EnableCors("FrontendOnly")]
|
[EnableCors("FrontendOnly")]
|
||||||
public sealed class ContactController : ControllerBase
|
public sealed class ContactController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly CaptchaSettings _captchaSettings;
|
|
||||||
private readonly ICaptchaVerifier _captcha;
|
private readonly ICaptchaVerifier _captcha;
|
||||||
private readonly IEmailSender _email;
|
private readonly IEmailSender _email;
|
||||||
private readonly ILogger<ContactController> _log;
|
private readonly ILogger<ContactController> _log;
|
||||||
|
|
||||||
public ContactController(IOptions<CaptchaSettings> options, ICaptchaVerifier captcha, IEmailSender email, ILogger<ContactController> log)
|
public ContactController(ICaptchaVerifier captcha, IEmailSender email, ILogger<ContactController> log)
|
||||||
{
|
{
|
||||||
_captchaSettings = options.Value;
|
|
||||||
_captcha = captcha;
|
_captcha = captcha;
|
||||||
_email = email;
|
_email = email;
|
||||||
_log = log;
|
_log = log;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the public reCAPTCHA site key used by the client to render
|
|
||||||
/// the reCAPTCHA widget and obtain client-side tokens.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>200 OK with the public site key as a string.</returns>
|
|
||||||
// ReCaptcha endpoints have been extracted to CaptchaController
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates the provided reCAPTCHA token and sends a contact message
|
/// Validates the provided reCAPTCHA token and sends a contact message
|
||||||
/// via the configured email sender.
|
/// via the configured email sender.
|
||||||
@@ -107,7 +97,5 @@ namespace Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Captcha verification helper was moved to CaptchaController; ContactController calls _captcha.VerifyAsync directly.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Api.Clients.Api.Contracts;
|
using Api.Clients.Api.Contracts;
|
||||||
using Api.Models.Requests;
|
using Api.Models.Requests;
|
||||||
|
using Api.Services.Contracts;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
using Swashbuckle.AspNetCore.Annotations;
|
using Swashbuckle.AspNetCore.Annotations;
|
||||||
@@ -16,23 +17,25 @@ public sealed class CvMatcherController : ControllerBase
|
|||||||
{
|
{
|
||||||
private readonly ICvMatcherApi _cvApi;
|
private readonly ICvMatcherApi _cvApi;
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly ICaptchaVerifier _captcha;
|
||||||
private readonly ILogger<CvMatcherController> _logger;
|
private readonly ILogger<CvMatcherController> _logger;
|
||||||
|
|
||||||
public CvMatcherController(
|
public CvMatcherController(
|
||||||
ICvMatcherApi cvApi,
|
ICvMatcherApi cvApi,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
|
ICaptchaVerifier captcha,
|
||||||
ILogger<CvMatcherController> logger)
|
ILogger<CvMatcherController> logger)
|
||||||
{
|
{
|
||||||
_cvApi = cvApi;
|
_cvApi = cvApi;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
|
_captcha = captcha;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Upload a CV PDF to the cv-matcher-api.
|
/// Upload a CV PDF to the cv-matcher-api.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cv">The uploaded CV PDF file.</param>
|
/// <param name="request">The uploaded CV request.</param>
|
||||||
/// <param name="gdprConsent">Whether the user consented to GDPR processing.</param>
|
|
||||||
/// <param name="ct">Cancellation token.</param>
|
/// <param name="ct">Cancellation token.</param>
|
||||||
[HttpPost("upload")]
|
[HttpPost("upload")]
|
||||||
[Consumes("multipart/form-data")]
|
[Consumes("multipart/form-data")]
|
||||||
@@ -61,6 +64,14 @@ public sealed class CvMatcherController : ControllerBase
|
|||||||
_logger.LogInformation("Proxying CV upload to cv-matcher-api. FileName={FileName}, Size={SizeBytes}, GdprConsent={GdprConsent}",
|
_logger.LogInformation("Proxying CV upload to cv-matcher-api. FileName={FileName}, Size={SizeBytes}, GdprConsent={GdprConsent}",
|
||||||
cv.FileName, cv.Length, gdprConsent);
|
cv.FileName, cv.Length, gdprConsent);
|
||||||
|
|
||||||
|
var userIp = HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||||
|
var verdict = await _captcha.VerifyAsync(request.CaptchaToken ?? string.Empty, userIp, ct);
|
||||||
|
if (!verdict.Success)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Captcha verification failed for CV upload. IP={IP}", userIp);
|
||||||
|
return BadRequest(new { error = "Captcha verification failed." });
|
||||||
|
}
|
||||||
|
|
||||||
var stream = cv.OpenReadStream();
|
var stream = cv.OpenReadStream();
|
||||||
var part = new Refit.StreamPart(stream, cv.FileName, "application/pdf");
|
var part = new Refit.StreamPart(stream, cv.FileName, "application/pdf");
|
||||||
using var response = await _cvApi.Upload(part, gdprConsent);
|
using var response = await _cvApi.Upload(part, gdprConsent);
|
||||||
@@ -100,6 +111,14 @@ public sealed class CvMatcherController : ControllerBase
|
|||||||
!string.IsNullOrWhiteSpace(request.JobUrl),
|
!string.IsNullOrWhiteSpace(request.JobUrl),
|
||||||
!string.IsNullOrWhiteSpace(request.JobDescription));
|
!string.IsNullOrWhiteSpace(request.JobDescription));
|
||||||
|
|
||||||
|
var userIp = HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||||
|
var verdict = await _captcha.VerifyAsync(request.CaptchaToken ?? string.Empty, userIp, ct);
|
||||||
|
if (!verdict.Success)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Captcha verification failed for job match. IP={IP}", userIp);
|
||||||
|
return BadRequest(new { error = "Captcha verification failed." });
|
||||||
|
}
|
||||||
|
|
||||||
using var response = await _cvApi.MatchJob(request);
|
using var response = await _cvApi.MatchJob(request);
|
||||||
return await ProxyResponseAsync(response, ct);
|
return await ProxyResponseAsync(response, ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Api.Models.Requests
|
||||||
|
{
|
||||||
|
public class CaptchaVerifyRequest
|
||||||
|
{
|
||||||
|
public string? Token { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,4 +6,5 @@ public sealed class JobMatchRequest
|
|||||||
public string? JobUrl { get; set; }
|
public string? JobUrl { get; set; }
|
||||||
public string? JobDescription { get; set; }
|
public string? JobDescription { get; set; }
|
||||||
public bool GdprConsent { get; set; }
|
public bool GdprConsent { get; set; }
|
||||||
|
public string? CaptchaToken { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ namespace Api.Models.Requests
|
|||||||
public IFormFile Cv { get; set; } = default!;
|
public IFormFile Cv { get; set; } = default!;
|
||||||
|
|
||||||
public bool GdprConsent { get; set; }
|
public bool GdprConsent { get; set; }
|
||||||
|
public string? CaptchaToken { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,6 @@
|
|||||||
public string? JobDescription { get; set; }
|
public string? JobDescription { get; set; }
|
||||||
public bool GdprConsent { get; set; }
|
public bool GdprConsent { get; set; }
|
||||||
public string? Email { get; set; }
|
public string? Email { get; set; }
|
||||||
|
public string? CaptchaToken { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: myai-cv-matcher-api
|
container_name: myai-cv-matcher-api
|
||||||
depends_on:
|
depends_on:
|
||||||
- mssql
|
|
||||||
- rag-api
|
- rag-api
|
||||||
ports:
|
ports:
|
||||||
- "8082:8080"
|
- "8082:8080"
|
||||||
@@ -92,9 +91,6 @@ services:
|
|||||||
- myai-network
|
- myai-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
|
||||||
myai-mssql-data:
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
myai-network:
|
myai-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
+14
-4
@@ -419,7 +419,12 @@
|
|||||||
}).then(postContact);
|
}).then(postContact);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
postContact('');
|
// Captcha unavailable: show clear error and restore UI
|
||||||
|
submitMSG(false, t('form.captchaFailed'));
|
||||||
|
formError();
|
||||||
|
loader.hide();
|
||||||
|
button.prop('disabled', false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -460,20 +465,24 @@
|
|||||||
}).then(postCv);
|
}).then(postCv);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
postCv('');
|
// Captcha unavailable: show clear error and restore UI
|
||||||
|
$msg.removeClass().addClass('form-message text-danger').text(t('form.captchaFailed'));
|
||||||
|
$button.prop('disabled', false).text(t('cv.submit'));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
function postCv(token) {
|
function postCv(token) {
|
||||||
try {
|
try {
|
||||||
var formData = new FormData();
|
var formData = new FormData();
|
||||||
formData.append('cv', file);
|
formData.append('cv', file);
|
||||||
formData.append('gdprConsent', String(consent));
|
formData.append('gdprConsent', String(consent));
|
||||||
|
formData.append('captchaToken', token || '');
|
||||||
var cvResponse = await fetch('/api/cv-matcher/upload', {
|
var cvResponse = await fetch('/api/cv-matcher/upload', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
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();
|
||||||
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'
|
||||||
@@ -482,7 +491,8 @@
|
|||||||
cvDocumentId: cvData.documentId || cvData.cvDocumentId,
|
cvDocumentId: cvData.documentId || cvData.cvDocumentId,
|
||||||
jobUrl: jobUrl,
|
jobUrl: jobUrl,
|
||||||
jobDescription: jobDescription,
|
jobDescription: jobDescription,
|
||||||
gdprConsent: consent
|
gdprConsent: consent,
|
||||||
|
captchaToken: token
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
if (!matchResponse.ok) throw new Error(t('cv.matchFailed'));
|
if (!matchResponse.ok) throw new Error(t('cv.matchFailed'));
|
||||||
|
|||||||
Reference in New Issue
Block a user